04. Spot instance¶
이 category는 ZLinkSpotManager·ZLinkRouteClient·ZLinkSpotPublisherClient가 제공하는 외부
진입점과, Spot 코드 안에서 ZLinkSpotContext/ZLinkInstanceSpotContext로 쓰는 진입점을 다룬다.
정확한 signature는
Java Spot exact interface가
소유한다.
ZLinkSpotManager.create¶
새 User Spot을 항상 새로 만든다. Framework가 새 global SpotId를 발급한다.
ZLinkSpotCreateResult created = spotManager.create("room")
.inMesh("play")
.request(new CreateRoom("ranked"))
.timeout(Duration.ofSeconds(5))
.submit()
.toCompletableFuture().get();
String spotId = created.spot().spotId();
옵션. 이 호출에는 다음 modifier가 붙는다.
| Modifier | 기본값 | 의미 |
|---|---|---|
.inMesh(meshName) |
Object Client·Server role의 Mesh가 하나면 생략 가능 | Spot을 생성할 Mesh. 후보가 둘 이상인데 생략하면 INVALID_OPERATION, 없으면 NOT_CONFIGURED, 지정한 Mesh가 없으면 NOT_FOUND |
.request(Object) / .request(ZLinkMessage) |
없음(빈 요청) | Spot의 onCreate(...)에 전달할 생성 요청 |
.timeout(Duration) |
resolve·factory·initialize 전체에 적용되는 기본값 | 생성 전체가 terminal state가 될 때까지의 상한 |
.submit() |
terminal(택 1) | 생성 완료까지 기다린다 |
.yield() |
terminal(택 1) | SpotWide handler 안에서만 유효 |
완료 결과. ZLinkSpotCreateResult.state()가 CREATED(새로 생성)다. Spot의 onCreate(...)가
거부하면 REJECTED이고 reply()에 거부 메시지가 담긴다. 같은 option을 두 번 설정하거나 terminal을
두 번 호출하면 INVALID_OPERATION, deadline 안에 끝나지 않으면 DEADLINE_EXCEEDED다.
선택 기준. 항상 새 인스턴스가 필요할 때 쓴다. 있으면 재사용하고 없을 때만 만들려면
getOrCreate를 쓴다.
ZLinkSpotManager.getOrCreate¶
지정한 SpotId의 Ready Spot이 있으면 그것을 반환하고, 없으면 새로 만든다.
ZLinkSpotCreateResult existingOrCreated = spotManager.getOrCreate("lobby-eu", "lobby")
.inMesh("play")
.request(new CreateLobby("eu"))
.submit()
.toCompletableFuture().get();
옵션. create와 동일하다 — .inMesh(...), .request(...), .timeout(...), terminal
.submit() 또는 .yield().
완료 결과. state()가 EXISTING이면 이미 있던 Spot을 그대로 반환하고 request는 무시한다.
CREATED면 새로 만든 것이다. 같은 SpotId가 creating 상태로 경합 중이면 그 결과를 기다렸다가
합류하고, cleanup으로 missing이 되면 새 reservation을 다시 경쟁한다.
선택 기준. SpotId로 멱등하게 "있으면 쓰고 없으면 만들기"가 필요할 때 쓴다. 항상 새 인스턴스가
필요하면 create를 쓴다.
find / close (manager)¶
기존 Spot을 조회하거나 정확한 incarnation을 닫는다.
Optional<SpotRef> spot = spotManager.find("lobby-eu").toCompletableFuture().get();
if (spot.isPresent()) {
boolean closed = spotManager.close(spot.get()).toCompletableFuture().get();
}
옵션. 두 호출 모두 modifier가 없다 — 대상 식별자만 받는다.
완료 결과. find는 Ready Spot이 없으면 Optional.empty()를 반환한다. close는 해당
incarnation이 없으면 false, generation이 다르면 INVALID_OPERATION, pre-commit seal 중이면
UNAVAILABLE이다. User Spot에 Actor membership이 남아 있으면 false이며 Actor를 자동으로
leave·destroy하지 않는다.
선택 기준. 지금 시점의 존재 여부 확인이나 명시적 종료가 필요할 때 쓴다. close는 stale
SpotRef로 다른 incarnation을 대신 닫지 않는다.
sendToSpot¶
Global SpotId 하나로 one-way message를 보낸다. 외부 client(ZLinkRouteClient)와 Spot 코드 안
(ZLinkSpotOutbound)이 같은 모양을 제공한다.
routeClient.sendToSpot("room-42", new PlayerJoinedRoom("player-1")).submit();
// Instance Spot을 필요하면 새로 활성화(cold activation)해서 보내는 경우
routeClient.sendToSpot("device-42", new DeviceCommand("reboot"))
.instanceSpot("device")
.submit();
옵션. 이 호출에는 다음 modifier가 붙는다.
| Modifier | 기본값 | 의미 |
|---|---|---|
.metadata(key, value) |
없음 | handler에 전달할 key-value |
.instanceSpot() |
없음(User Spot만 resolve) | Missing이면 cold activation한다. Existing authority가 있으면 stable type 수와 관계없이 저장된 type을 사용한다 |
.instanceSpot(stableType) |
— | Missing인데 등록 타입이 여럿이면 stable type을 명시해야 한다 |
.inMesh(meshName) |
Object Client·Server role의 Mesh가 하나면 생략 가능 | Missing Instance Spot을 처음 만들 Mesh. Instance marker 없이 쓰면 INVALID_OPERATION |
.submit() |
필수 terminal | source-local admission까지만 기다린다 |
완료 결과. SpotId가 없고 Instance marker도 없으면 NOT_FOUND. .instanceSpot(...)을 썼는데
existing authority가 User Spot이거나 명시한 타입과 다르면 TYPE_MISMATCH. 그 외 완료 kind는
messaging-execution category의 공통 규칙과 같다.
선택 기준. Reply가 필요 없는 Spot 메시징에 쓴다. Reply가 필요하면 requestToSpot을 쓴다.
requestToSpot¶
Global SpotId 하나로 typed request/reply를 주고받는다.
CompletionStage<RoomState> reply = routeClient
.requestToSpot("room-42", new GetRoomState())
.timeout(Duration.ofSeconds(3))
.submit(RoomState.class);
옵션. sendToSpot과 동일한 .instanceSpot(...)/.inMesh(...)에 더해 다음이 있다.
| Modifier | 기본값 | 의미 |
|---|---|---|
.timeout(Duration) |
MeshNode의 request 기본 timeout | resolve, cold activation, handler, reply 전체의 deadline |
.submit(TReply.class) |
terminal(택 1) | reply 수신까지 기다린다 |
.yield(TReply.class) |
terminal(택 1) | SpotWide User Spot·Instance Spot handler 안에서만 유효. 그 밖에서 호출하면 INVALID_OPERATION |
완료 결과. sendToSpot과 같은 실패 kind에 더해, cold activation 중 factory나 initialize가
실패하면 typed failure로 완료된다 — Framework가 내부적으로 재시도하지 않는다.
선택 기준. Reply 값이 필요할 때 쓴다. One-way면 sendToSpot을 쓴다.
publish (Spot Logical Multicast)¶
ChannelName과 topic으로 구독자에게 typed event를 발행한다. ZLinkSpotPublisherClient(외부)와
ZLinkSpotOutbound(Spot 코드 안)가 같은 모양을 제공한다.
옵션. 이 호출에는 .metadata(...)와 필수 terminal .submit()이 있다 — topic은 필수 인자다.
완료 결과. 정상 완료는 발행 admission이 끝났다는 뜻이다. Subscriber 수신은 기다리지 않는다.
messaging-execution category의 classic fanout publish와 달리, ChannelName만으로 owner MeshNode를
결정하며 caller가 MeshName을 추가로 넘기지 않는다.
선택 기준. Spot 상태 변화를 관찰자에게 알릴 때 쓴다. 구독자에게 직접 reply가 필요하면 이
항목이 아니라 requestToSpot을 쓴다.
addTimer (Spot 코드 안)¶
Spot에 속한 주기 timer를 등록한다. ZLinkSpotContext.addTimer(...)로 호출한다.
ZLinkTimer timer = context.addTimer(
"room-tick",
Duration.ofSeconds(1),
RoomTickHandler.class,
new ZLinkTimerOptions(ZLinkTimerOverrunPolicy.SKIP_LATE_TICKS, 1, false))
.toCompletableFuture().get();
옵션. ZLinkTimerOptions의 component는 다음과 같다.
| Component | 기본값 | 의미 |
|---|---|---|
overrunPolicy |
SKIP_LATE_TICKS |
tick이 밀렸을 때 건너뛸지, 상한 안에서 따라잡을지, 다음 tick을 늦출지 |
maxCatchUpTicks |
1 | CATCH_UP_BOUNDED일 때 한 번에 따라잡을 최대 tick 수 |
stopOnUnhandledException |
false |
handler 예외 시 timer를 멈출지 여부 |
완료 결과. ZLinkTimer를 반환한다. Timer는 이 Spot에 속한 logical registration이라 relocation
때 자동으로 이전되며 application이 target에서 다시 등록할 필요가 없다. cancel() 또는
close()(AutoCloseable)로 취소한다.
선택 기준. Spot 안에서 주기 작업이 필요할 때 쓴다.
runCpuWorker / runIoWorker (Spot 코드 안)¶
Spot의 owner turn을 막지 않고 별도 worker에서 작업을 실행한다.
CompletionStage<Integer> result = context
.runCpuWorker(cancellation -> computeExpensiveScore(cancellation))
.timeout(Duration.ofSeconds(2))
.submit();
옵션. ZLinkWorkerCall<T>가 제공하는 modifier는 다음과 같다.
| Modifier | 기본값 | 의미 |
|---|---|---|
.timeout(Duration) |
Worker option의 기본값 | 작업 완료 상한 |
.submit() |
terminal(택 1) | 완료까지 기다린다 |
.yield() |
terminal(택 1) | SpotWide handler 안에서만 유효 |
완료 결과. T를 반환하거나 timeout이면 DEADLINE_EXCEEDED로 완료한다. Worker pool 크기와
idle timeout은 host 시작 전에만(configureWorkers()) 설정한다.
선택 기준. CPU-bound 계산은 runCpuWorker, I/O 대기가 있는 작업(ZLinkIoWorkerTask —
CompletionStage<T> 반환)은 runIoWorker를 쓴다. 둘 다 owner turn의 순차 실행을 막지 않으려는
목적이다.
Handler 등록 (Spot 코드 안, configure())¶
Spot이 받을 packet·request·구독·member Actor 메시지를 처리할 handler를 등록한다.
ZLinkSpotContext.handlers()(User Spot)/ZLinkInstanceSpotContext.handlers()(Instance Spot)로
호출하며, configure() override 안에서만 호출한다.
옵션. Handler가 처리하는 대상에 따라 구현하는 interface와 annotation이 갈린다.
| 대상 | Handler interface | 식별 annotation |
|---|---|---|
| User Spot 앞 one-way packet | ZLinkSpotPacketHandler<TSpot, TMessage> |
@ZLinkPacket |
| User Spot 앞 request | ZLinkSpotRequestHandler<TSpot, TRequest, TReply> |
@ZLinkSpotRequest |
| Logical Multicast 구독 이벤트 | ZLinkSpotSubscriptionHandler<TSpot, TEvent> |
@ZLinkSpotSubscription(spotNodeName, topic) |
| User Spot의 member Actor 앞 one-way packet | ZLinkSpotActorSendHandler<TSpot, TActor, TMessage> |
@ZLinkSpotActorSend |
| User Spot의 member Actor 앞 request | ZLinkSpotActorRequestHandler<TSpot, TActor, TRequest, TReply> |
@ZLinkSpotActorRequest |
| Spot 주기 timer | ZLinkSpotTimerHandler<TSpot> |
@ZLinkSpotTimer(name, periodMillis) |
| Instance Spot 앞 packet | User Spot의 packet handler와 같은 모양 | @ZLinkPacket |
ZLinkSpotHandlerRegistry.addHandler(Class<?>)와
ZLinkInstanceSpotHandlerRegistry.addPacket(Class<?>)는 handler 종류를 구분하지 않는 단일 등록
메서드다 — annotation과 구현 interface로 실제 역할을 판별한다.
완료 결과. 반환값 없이 동기로 등록된다. Packet name을 생략하면 annotation의 value()/
packetName()을 쓰고, annotation도 없으면 타입 이름을 쓴다. 같은 owner의 handler key 중복은
startup 검증에서 ZLinkConfigurationException으로 드러난다.
선택 기준. configure()가 호출될 때마다 이 Spot이 처리할 모든 handler를 등록한다.
Node·Channel handler는 topology-discovery category의 등록 항목을, STREAM session handler는
stream-session category를 참고한다.
outbound() — sendToChannel / requestToChannel (Spot 코드 안)¶
Spot 코드 안에서 ChannelName으로 one-way message를 보내거나 typed request/reply를 주고받는다.
ZLinkSpotContext.outbound()가 반환하는 ZLinkSpotOutbound가 제공하며 messaging-execution
category의 sendToChannel/requestToChannel과 같은 모양이다.
CompletionStage<Leaderboard> reply = context.outbound()
.requestToChannel("leaderboard.api", new GetLeaderboard())
.submit(Leaderboard.class);
옵션. messaging-execution category의 sendToChannel/requestToChannel과 동일한 modifier를
받는다.
완료 결과. messaging-execution category의 완료 kind와 같다.
선택 기준. Spot이 외부 client가 아니라 자기 코드 안에서 다른 ChannelName의 handler를 호출해야
할 때 쓴다. 다른 Spot을 직접 호출하려면 sendToSpot/requestToSpot을 쓴다.
leaveActor / close / destroyActor (Spot 코드 안, 종료·이탈)¶
Member Actor를 이 Spot에서 내보내거나, Spot 자신을 닫거나, Entry Spot에서 Actor를 파기한다.
context.leaveActor(actor).toCompletableFuture().get(); // User Spot: member Actor만 내보낸다
boolean closed = context.close().toCompletableFuture().get(); // User·Instance Spot: 이 Spot 자신을 닫는다
entryContext.destroyActor(actor).toCompletableFuture().get(); // Entry Spot: Actor를 완전히 파기한다
옵션. 세 호출 모두 modifier가 없다 — 대상(leaveActor/destroyActor)만 받는다.
완료 결과. leaveActor(ZLinkSpotContext 전용)는 member Actor membership만 해제하고 Actor
자체는 파기하지 않는다. close(ZLinkSpotContext/ZLinkInstanceSpotContext)는 manager의
close(spotRef)(spot-instance category 앞부분 항목)와 같은 완료 kind를 쓰되, 이 Spot 자신을
대상으로 한다. destroyActor(ZLinkEntrySpotContext 전용)는 Actor를 완전히 파기한다 —
leaveActor와 달리 membership 해제가 아니라 Actor 자체를 없앤다.
선택 기준. Member Actor를 다른 곳으로 옮기지 않고 이 Spot에서만 빼려면 leaveActor를, Spot
자신을 스스로 종료하려면 close를, Entry Spot에서 더 이상 필요 없는 Actor를 완전히 없애려면
destroyActor를 쓴다.
relocationReady().defer() (Spot 코드 안)¶
APPLICATION_SIGNALED coordination mode를 선택한 SPOT_WIDE Spot에서, relocation 경계를 다음
application turn 앞으로 미룬다.
옵션. 이 호출에는 modifier가 없다.
완료 결과. 반환값 없음. 현재 handler가 끝난 뒤 relocation 경계를 등록한다. 이동하지 않았거나
commit 전에 abort했으면 source에서 CONTINUED, 이동했으면 target에서 RELOCATED completion을
onRelocationReadyCompleted(...)로 받는다. FRAMEWORK_MANAGED mode, PER_ACTOR Spot, Entry·
Instance Spot, Spot turn 밖, 같은 turn의 중복 호출은 INVALID_OPERATION으로 완료한다.
선택 기준. Application이 relocation 시점을 특정 turn 경계로 정밀하게 제어해야 할 때 쓴다.
기본 FRAMEWORK_MANAGED mode에서는 이 호출이 필요하지 않다.
전체 근거는 Java Spot exact interface를 참고한다.