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++
// ============================================================================
// 시나리오 (서버-서버)
// ----------------------------------------------------------------------------
// 운영 중인 서버 노드를 점검하기 위해, 그 노드가 담당하던 플레이어 세션을
// 다른(신규) 노드로 핫마이그레이션(이관)한다. 이관은 대략 다음 순서로
// 진행된다.
// 1) 구 노드가 세션 상태(위치, 인벤토리 캐시 등)를 신 노드로 전송한다.
// 2) 신 노드가 상태를 적재하고 이 세션을 "담당 시작"으로 표시한다.
// 3) 라우팅 테이블(어느 노드가 이 세션을 담당하는지)을 신 노드로 갱신한다.
//
// 이관이 진행되는 동안에도 클라이언트로부터 패킷은 계속 도착할 수 있다.
// 구 노드에 이미 도착해 처리 대기 중이던 패킷과, 라우팅 갱신 이후 신 노드로
// 새로 도착하는 패킷이 시간상 겹칠 수 있다.
//
// 요구사항:
// - 이관 중 도착한 패킷이라도 정확히 한 노드(구 노드 또는 신 노드 중
// 하나)에서만 처리되어야 한다. 양쪽에서 중복 처리되거나, 양쪽 모두에서
// 처리되지 않고 유실되어서는 안 된다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
// ============================================================================
#include <cstdint>
#include <string>
#include <vector>
#include <atomic>
struct SessionState
{
uint64_t sessionId;
std::vector<uint8_t> serializedState;
};
enum class SessionOwnership { Owned, Migrating, NotOwned };
class IRoutingTable
{
public:
virtual ~IRoutingTable() = default;
virtual void UpdateOwner(uint64_t sessionId, const std::string& nodeId) = 0;
};
class IStateTransferChannel
{
public:
virtual ~IStateTransferChannel() = default;
virtual void SendState(const std::string& targetNodeId, const SessionState& state) = 0;
};
// 구 노드에서 실행되는 이관 처리기.
class MigrationSource
{
public:
MigrationSource(IStateTransferChannel& transferChannel, IRoutingTable& routingTable,
std::string myNodeId)
: transferChannel_(transferChannel), routingTable_(routingTable),
myNodeId_(std::move(myNodeId)), ownership_(SessionOwnership::Owned)
{
}
// 이관을 시작한다. 이관이 끝날 때까지도 이 노드는 계속 패킷을
// 받아들여 처리할 수 있다.
void MigrateTo(uint64_t sessionId, const std::string& targetNodeId, const SessionState& state)
{
transferChannel_.SendState(targetNodeId, state);
// (A) 상태 전송이 끝난 시점부터 라우팅 테이블 갱신이 실제로
// 반영되기까지, 이 구 노드는 여전히 자신을 담당자로 여기고
// 들어오는 패킷을 계속 처리한다. 이 구간 동안 자신의 처리
// 여부를 막거나 신 노드와 조율하지 않는다.
routingTable_.UpdateOwner(sessionId, targetNodeId);
ownership_.store(SessionOwnership::NotOwned, std::memory_order_relaxed);
}
// 패킷 수신 핸들러: 이 노드가 세션을 담당 중이라 믿는 동안 계속
// 호출될 수 있다.
bool TryHandlePacket(uint64_t sessionId, const std::vector<uint8_t>& packet)
{
if (ownership_.load(std::memory_order_relaxed) == SessionOwnership::NotOwned)
return false;
ProcessPacket(sessionId, packet);
return true;
}
private:
void ProcessPacket(uint64_t /*sessionId*/, const std::vector<uint8_t>& /*packet*/)
{
// 처리 로직 생략
}
IStateTransferChannel& transferChannel_;
IRoutingTable& routingTable_;
std::string myNodeId_;
std::atomic<SessionOwnership> ownership_;
};
// 신 노드에서 실행되는 이관 수신기.
class MigrationTarget
{
public:
MigrationTarget() : ownership_(SessionOwnership::NotOwned) {}
// 구 노드로부터 상태를 전달받으면 즉시 자신을 담당자로 표시하고
// 패킷 처리를 시작한다.
void OnStateReceived(uint64_t sessionId, const SessionState& state)
{
LoadState(state);
// (B) 라우팅 테이블이 실제로 이 노드를 가리키도록 갱신되었는지와
// 무관하게, 상태를 받은 즉시 스스로 담당자로 표시하고 들어오는
// 패킷 처리를 시작한다. 구 노드가 아직 자신을 담당자로 여기고
// 있을 수 있다는 점은 고려하지 않는다.
ownership_.store(SessionOwnership::Owned, std::memory_order_relaxed);
}
bool TryHandlePacket(uint64_t sessionId, const std::vector<uint8_t>& packet)
{
if (ownership_.load(std::memory_order_relaxed) != SessionOwnership::Owned)
return false;
ProcessPacket(sessionId, packet);
return true;
}
private:
void LoadState(const SessionState& /*state*/)
{
// 상태 역직렬화 및 적재 (구현 생략)
}
void ProcessPacket(uint64_t /*sessionId*/, const std::vector<uint8_t>& /*packet*/)
{
// 처리 로직 생략
}
std::atomic<SessionOwnership> ownership_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.