44. 버프 슬롯 비트마스크 CAS 루프의 재시도 결함 (C#)

난이도 중 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMORPG 존(Zone) 서버의 "버프 슬롯 레지스트리"다. 플레이어마다 32칸짜리 버프
// 슬롯이 있고, 사용 중인 칸을 int 비트마스크 하나(SlotMask)로 관리한다. 버프 부여
// 요청은 IOCP 워커 스레드 여러 개에서 동시에 들어온다(광역 힐, 파티 오라, 필드
// 장판이 같은 tick 에 겹친다). 또 하나의 공유 상태로 "인스턴스 던전 입장 토큰"이
// 있어 존이 동시에 만들 수 있는 인스턴스 수를 제한한다. 둘 다 초당 수만 번
// 갱신되므로 lock 없이 Interlocked 기반 CAS 루프로 구현되어 있다.
//
// 요구사항: R1 슬롯당 버프 1개  R2 마스크에 반영된 경우에만 통계·이벤트 정확히 1회
//           R3 토큰 잔량 음수 금지, 발급 로그 건수 = 실제 발급 수
//           R4 부하 시에도 워커 스레드가 코어를 독점하지 않을 것
//
// 과제: (A)/(B) 마커 지점을 중심으로 무엇이 잘못되었는지, 어떤 상황에서 요구사항이
//       깨지는지, 어떻게 고칠지 설명하라.
// ============================================================================

using System;
using System.Collections.Concurrent;
using System.Threading;

namespace ZoneServer.Buffs
{
    public struct BuffPayload { public int BuffId; public int Stack; public long ExpireTicks; }

    public sealed class BuffAppliedEvent
    {
        public long PlayerId; public int Slot; public int MaskAfter; public DateTime AtUtc;
    }

    public sealed class TokenGrant
    {
        public long RequestId; public int Cost; public long RemainAfter;
    }

    public sealed class ZoneMetrics
    {
        private long _buffApplied, _buffRejected, _tokenGranted;

        public void IncBuffApplied() => Interlocked.Increment(ref _buffApplied);
        public void IncBuffRejected() => Interlocked.Increment(ref _buffRejected);
        public void IncTokenGranted() => Interlocked.Increment(ref _tokenGranted);

        public long BuffApplied => Interlocked.Read(ref _buffApplied);
        public long BuffRejected => Interlocked.Read(ref _buffRejected);
        public long TokenGranted => Interlocked.Read(ref _tokenGranted);
    }

    public sealed class PlayerBuffState
    {
        public long PlayerId;
        public int SlotMask;
        public readonly BuffPayload[] Payloads = new BuffPayload[32];
    }

    public sealed class BuffRegistry
    {
        private readonly ConcurrentDictionary<long, PlayerBuffState> _states = new();
        private readonly ConcurrentQueue<BuffAppliedEvent> _events = new();
        private readonly ConcurrentQueue<TokenGrant> _grantLog = new();
        private readonly ZoneMetrics _metrics = new();

        private long _entryTokens = 64;
        private long _nextRequestId;

        public bool TryApplyBuff(long playerId, int slot, BuffPayload payload)
        {
            if (slot < 0 || slot >= 32) return false;

            var st = _states.GetOrAdd(playerId, id => new PlayerBuffState { PlayerId = id });
            int bit = 1 << slot;

            int observed = st.SlotMask;                                  // (A)

            while (true)
            {
                if ((observed & bit) != 0)
                {
                    _metrics.IncBuffRejected();
                    return false;
                }

                int desired = observed | bit;
                st.Payloads[slot] = payload;

                _metrics.IncBuffApplied();                               // (B)
                _events.Enqueue(new BuffAppliedEvent
                {
                    PlayerId = playerId, Slot = slot,
                    MaskAfter = desired, AtUtc = DateTime.UtcNow,
                });

                int prev = Interlocked.CompareExchange(ref st.SlotMask, desired, observed);
                if (prev == observed)
                    return true;

                // 다른 스레드가 마스크를 바꾼 경우 다시 시도한다.
            }
        }

        public bool TryAcquireEntryToken(int cost)
        {
            if (cost <= 0) return false;

            long observed = _entryTokens;

            while (true)
            {
                if (observed < cost)
                    return false;

                long desired = observed - cost;
                long requestId = Interlocked.Increment(ref _nextRequestId);

                _metrics.IncTokenGranted();                              // (B)
                _grantLog.Enqueue(new TokenGrant
                {
                    RequestId = requestId, Cost = cost, RemainAfter = desired,
                });

                long prev = Interlocked.CompareExchange(ref _entryTokens, desired, observed);
                if (prev == desired)                                     // (A)
                    return true;

                observed = _entryTokens;
            }
        }

        public void ReleaseEntryToken(int cost) => Interlocked.Add(ref _entryTokens, cost);

        public bool TryClearBuff(long playerId, int slot)
        {
            if (!_states.TryGetValue(playerId, out var st)) return false;

            int bit = 1 << slot;
            int observed = st.SlotMask;
            int desired = observed & ~bit;

            return Interlocked.CompareExchange(ref st.SlotMask, desired, observed) == observed;
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.