Repository navigation
Conversation
|
|
||
| enum ArcusFutureState { | ||
| /** | ||
| * Waiting for the complete response. |
There was a problem hiding this comment.
complete response 보다는 response from cache server 이런 표현이 낫지 않을까요?
There was a problem hiding this comment.
Waiting for a response from the cache server. 로 수정했습니다.
| } | ||
|
|
||
| private void finishResponse(T value) { | ||
| if (state.compareAndSet(RESPONSE_RECEIVED, DECIDED)) { |
There was a problem hiding this comment.
DECIDED 상태를 두는 대신 CompletableFuture 자체에서 관리하는 result(isDone으로 완료 여부를 판별) 기반 방식을 사용하지 못한 이유가 있나요?
future state <-> complete 간의 상태도 고려해야하면 처리가 복잡해져서 최대한 completableFuture자체의 상태를 활용하는 형태가 좋을 것 같습니다.
There was a problem hiding this comment.
우선, 두 가지 이유로 인해서 ArcusFutureState 라는 별도의 상태 관리를 뒀으며 아래의 설명을 참고해주시면 좋을 것 같습니다.
1. timeout 범위를 구분하는 상태가 필요하다.
ArcusFuture의 타임아웃은 addOp() 직후부터 전체 응답 수신까지이고, Future는 그 뒤 디코딩이 끝나야 완료됩니다.
응답 대기 중 → isDone() == false, timeout 적용
응답 수신, 디코딩 중 → isDone() == false, timeout 적용 '제외'
따라서 isDone() 만으로 타임아웃 적용 여부를 판단할 수 없습니다. 응답 수신 시 타이머를 취소하더라도 이미 실행을 시작한 timeout task 와 경합할 수 있으므로, 응답과 타임아웃 중 어느 쪽이 선점했는지 원자적으로 판단해야 합니다.
이로 인해, WAITING_RESPONSE -> RESPONSE_RECEIVED 상태 전이가 필요합니다.
2. DECIDED 는 Future 완료 전에 결과를 확정하기 위해 필요하다.
현재 타임아웃 처리는 다음 순서로 진행됩니다.
timeout 선점
-> node 연속 timeout 갱신 (`MemcachedConnection.opTimedOut(op)`)
-> Operation 취소 (`op.cancel("by operation timeout")`)
-> completion executor에서 Future 예외 완료
이 과정에서 타임아웃 처리를 시작했지만, isDone()은 여전히 false인 구간이 생깁니다. 이 때 취소를 허용하게 된다면, 타임아웃 결과가 CancellationException 으로 바뀔 수 있습니다.
따라서 디코딩 중에는 사용자 취소를 허용하되, 타임아웃이 이미 확정된 뒤에는 취소를 거부하도록 RESPONSE_RECEIVED와 DECIDED를 구분했습니다.
다만, 디코딩 중 취소를 허용하지 않는다면 두 상태를 "선점됨" 하나로 합쳐서 단순화할 수 있을 것 같기는 합니다.
| op.cancel("by application"); | ||
| } | ||
| } finally { | ||
| cancelled = super.cancel(false); |
There was a problem hiding this comment.
기존과 다르게 complete된 op에 대해서 super.cancel 해주는 이유가 무엇인가요?
There was a problem hiding this comment.
이 부분도 Opeartion 완료 시점과 Future 완료 시점이 다르다는 점과 관련이 있습니다.
전체 응답을 받으면 Operation은 완료되지만, Future는 디코딩을 기다리거나 디코딩하는 동안 아직 미완료 상태입니다.
기존에는 op.cancel()이 호출하는 cancel 콜백을 통해 Future를 취소했지만, 이미 완료된 Operation은 op.cancel()이 false를 반환하고 callback도 호출하지 않아 Future의 상태가 isDone() == false 인데도 디코딩 중인 Future를 취소할 수 없었습니다.
따라서 응답을 받은 뒤에는 super.cancel()로 Future를 직접 취소하고, 디코딩 전이면 디코딩을 건너뛰고 디코딩 중이면 해당 결과를 버리는 방식으로 수정했습니다.
| cancelTimeoutTask(); | ||
| super.cancel(false); |
There was a problem hiding this comment.
여기 두줄은 future.cancel 호출했을 때 중복 호출이 가능한데 이를 방지할 수 있는 방법이 있나요?
There was a problem hiding this comment.
future.cancel() 에 의해 internalCancel() 이 호출될 수는 있지만, 그 전에 상태를 DECIDED로 변경하므로 위의 상태를 검사하는 if문에서 실패하여 해당 코드로 도달하지 못하게 됩니다.
반대 순서의 경우에도 cancel() 의 상태 검사에서 걸리게되어 두 메서드간 중복 실행은 발생하지 않습니다.
| MemcachedConnection.opTimedOut(op); | ||
| op.cancel("by operation timeout"); | ||
|
|
||
| ArcusExecutors.COMPLETION_EXECUTOR.execute(() -> super.completeExceptionally(exception)); |
There was a problem hiding this comment.
super.completeExceptionally 호출은 금방끝나는 작업이라 completion 스레드에 맡기지 않아도 될 것 같은데,
이렇게 구성한 이유가 무엇인가요?
There was a problem hiding this comment.
super.completeExceptionally()는 Future의 상태만 바꾸는 것이 아니라, 사용자가 등록한 exceptionally()나 whenComplete() 같은 후속 작업을 호출한 스레드에서 바로 실행할 수 있습니다. 그래서 호출이 빨리 끝난다고 보장하기는 어려워 보입니다.
또한 TIMEOUT_SCHEDULER는 단일 스레드에서 모든 Operation의 timeout()을 실행하므로, 여기서 사용자 코드가 오래 실행되면 다른 Operation의 timeout 처리까지 지연됩니다.
이를 막기 위해 Future 예외 완료를 COMPLETION_EXECUTOR에 위임하도록 구성했습니다.
There was a problem hiding this comment.
물론, COMPLETION_EXECUTOR 가 예외 처리를 하게 되어 디코딩 작업이 밀릴 순 있으나, 이는 추후에 고려하여 설계하는 방향으로 진행하고자 합니다.
2fa366b to
eafc550
Compare
🔗 Related Issue
⌨️ What I did
ScheduledFuture기반 operation timeout을 추가해, Future 조회 여부와 관계없이 응답이 없는 요청을 정리하도록 했습니다.ConnectionFactoryBuilder.setOpTimeout()설정을 사용하고,withOperationTimeout()으로 별도 timeout을 지정할 수 있도록 했습니다.동작 정리
timeout 범위
flowchart LR P["Transcoder 인코딩<br/>Operation 생성"] --> A["addOp() 반환<br/>타이머 등록"] --> B["write queue 대기"] --> C["전송 · 서버 처리"] --> D["전체 응답 수신<br/>타이머 해제"] D --> E["Transcoder 디코딩"] --> F["Future 완료"] --> G["thenApply 등<br/>후속 stage"] subgraph BEFORE ["범위 밖 · 호출 스레드"] P end subgraph IN ["operation timeout 범위"] A B C D end subgraph AFTER ["범위 밖"] E F G endtimeout은
addOp()반환 직후부터 전체 응답 수신까지입니다. 인코딩은 타이머 등록 전에, 디코딩과 후속 stage는 타이머 해제 후에 실행됩니다.상태 전이
stateDiagram-v2 [*] --> WAITING_RESPONSE WAITING_RESPONSE --> RESPONSE_RECEIVED: 전체 응답 수신 WAITING_RESPONSE --> DECIDED: timeout / cancel / 내부 취소 RESPONSE_RECEIVED --> DECIDED: 디코딩 완료 / cancel DECIDED --> [*] note right of WAITING_RESPONSE cancel 시 Operation도 취소 end note note right of RESPONSE_RECEIVED timeout 무시 cancel 시 결과만 폐기 end note응답, timeout, 취소 중
compare-and-set에 성공한 쪽만 작업(node 갱신, Operation 취소, Future 완료)을 실행하고, 이후 도착한 응답이나 취소는 무시됩니다.