33. 세션 핫마이그레이션 중 이중 처리와 소유권 공백

난이도 중 해설 보기 →

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

결함 코드 · C#
// 시나리오 (서버-서버): 운영 중인 서버 노드를 점검하기 위해, 그 노드가
// 담당하던 플레이어 세션을 다른(신규) 노드로 핫마이그레이션(이관)한다.
// 이관은 대략 다음 순서로 진행된다.
//   1) 구 노드가 세션 상태(위치, 인벤토리 캐시 등)를 신 노드로 전송한다.
//   2) 신 노드가 상태를 적재하고 이 세션을 "담당 시작"으로 표시한다.
//   3) 라우팅 테이블(어느 노드가 이 세션을 담당하는지)을 신 노드로 갱신한다.
//
// 이관이 진행되는 동안에도 클라이언트로부터 패킷은 계속 도착할 수 있다.
// 구 노드에 이미 도착해 처리 대기 중이던 패킷과, 라우팅 갱신 이후 신 노드로
// 새로 도착하는 패킷이 시간상 겹칠 수 있다.
//
// 요구사항:
//   - 이관 중 도착한 패킷이라도 정확히 한 노드(구 노드 또는 신 노드 중
//     하나)에서만 처리되어야 한다. 양쪽에서 중복 처리되거나, 양쪽 모두에서
//     처리되지 않고 유실되어서는 안 된다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.

using System.Threading.Tasks;

public sealed class SessionState
{
    public ulong SessionId;
    public byte[] SerializedState;
}

public enum SessionOwnership { Owned, Migrating, NotOwned }

public interface IRoutingTable
{
    Task UpdateOwnerAsync(ulong sessionId, string nodeId);
}

public interface IStateTransferChannel
{
    Task SendStateAsync(string targetNodeId, SessionState state);
}

public interface IPacketQueue
{
    // 아직 처리되지 않은 대기 중인 패킷들을 꺼내 반환한다.
    System.Collections.Generic.IReadOnlyList<byte[]> DrainPending(ulong sessionId);
}

// 구 노드에서 실행되는 이관 처리기.
public sealed class MigrationSource
{
    private readonly IStateTransferChannel _transferChannel;
    private readonly IRoutingTable _routingTable;
    private readonly IPacketQueue _packetQueue;
    private readonly string _myNodeId;

    private SessionOwnership _ownership = SessionOwnership.Owned;

    public MigrationSource(IStateTransferChannel transferChannel, IRoutingTable routingTable,
        IPacketQueue packetQueue, string myNodeId)
    {
        _transferChannel = transferChannel;
        _routingTable = routingTable;
        _packetQueue = packetQueue;
        _myNodeId = myNodeId;
    }

    // 이관을 시작한다. 이관이 끝날 때까지도 이 노드는 계속 패킷을
    // 받아들여 처리할 수 있다.
    public async Task MigrateToAsync(ulong sessionId, string targetNodeId, SessionState state)
    {
        await _transferChannel.SendStateAsync(targetNodeId, state);

        // (A) 상태 전송이 끝난 시점부터 라우팅 테이블 갱신이 실제로
        //     반영되기까지, 이 구 노드는 여전히 자신을 담당자로 여기고
        //     들어오는 패킷을 계속 처리한다. 이 구간 동안 자신의 처리
        //     여부를 막거나 신 노드와 조율하지 않는다.
        await _routingTable.UpdateOwnerAsync(sessionId, targetNodeId);

        _ownership = SessionOwnership.NotOwned;
    }

    // 패킷 수신 핸들러: 이 노드가 세션을 담당 중이라 믿는 동안 계속
    // 호출될 수 있다.
    public bool TryHandlePacket(ulong sessionId, byte[] packet)
    {
        if (_ownership == SessionOwnership.NotOwned)
            return false;

        // 실제 게임 로직 처리 (구현 생략)
        ProcessPacket(sessionId, packet);
        return true;
    }

    private void ProcessPacket(ulong sessionId, byte[] packet)
    {
        // 처리 로직 생략
    }
}

// 신 노드에서 실행되는 이관 수신기.
public sealed class MigrationTarget
{
    private SessionOwnership _ownership = SessionOwnership.NotOwned;

    // 구 노드로부터 상태를 전달받으면 즉시 자신을 담당자로 표시하고
    // 패킷 처리를 시작한다.
    public void OnStateReceived(ulong sessionId, SessionState state)
    {
        LoadState(state);

        // (B) 라우팅 테이블이 실제로 이 노드를 가리키도록 갱신되었는지와
        //     무관하게, 상태를 받은 즉시 스스로 담당자로 표시하고 들어오는
        //     패킷 처리를 시작한다. 구 노드가 아직 자신을 담당자로 여기고
        //     있을 수 있다는 점은 고려하지 않는다.
        _ownership = SessionOwnership.Owned;
    }

    public bool TryHandlePacket(ulong sessionId, byte[] packet)
    {
        if (_ownership != SessionOwnership.Owned)
            return false;

        ProcessPacket(sessionId, packet);
        return true;
    }

    private void LoadState(SessionState state)
    {
        // 상태 역직렬화 및 적재 (구현 생략)
    }

    private void ProcessPacket(ulong sessionId, byte[] packet)
    {
        // 처리 로직 생략
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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