Skip to content

FEATURE: Add operation timeouts to the v2 API - #1118

Open
f1v3-dev wants to merge 1 commit into
naver:developfrom
f1v3-dev:feat/v2-op-timeout
Open

f1v3-dev wants to merge 1 commit into
naver:developfrom
f1v3-dev:feat/v2-op-timeout

Conversation

@f1v3-dev

@f1v3-dev f1v3-dev commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

🔗 Related Issue

⌨️ What I did

  • v2 API에 ScheduledFuture 기반 operation timeout을 추가해, Future 조회 여부와 관계없이 응답이 없는 요청을 정리하도록 했습니다.
  • 기본 timeout은 ConnectionFactoryBuilder.setOpTimeout() 설정을 사용하고, withOperationTimeout()으로 별도 timeout을 지정할 수 있도록 했습니다.
  • timeout 만료 시 node 연속 timeout 갱신 → Operation 취소 → Future 예외 완료 순으로 처리합니다.
  • 전체 응답 수신 또는 취소 시 타이머를 해제하고, timeout 범위에서 결과 디코딩과 후속 CompletionStage 실행을 제외했습니다.
  • timeout 동작, 타이머 정리, Future를 조회하지 않는 경우에 대한 테스트를 추가했습니다.

동작 정리

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
    end
Loading

timeout은 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
Loading

응답, timeout, 취소 중 compare-and-set에 성공한 쪽만 작업(node 갱신, Operation 취소, Future 완료)을 실행하고, 이후 도착한 응답이나 취소는 무시됩니다.

@f1v3-dev
f1v3-dev requested a review from oliviarla October 2, 2026 02:51
@f1v3-dev f1v3-dev self-assigned this Oct 2, 2026
Comment thread src/main/java/net/spy/memcached/v2/ArcusFutureState.java Outdated
Comment thread src/main/java/net/spy/memcached/v2/ArcusFutureImpl.java
op.cancel("by application");
}
} finally {
cancelled = super.cancel(false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존과 다르게 complete된 op에 대해서 super.cancel 해주는 이유가 무엇인가요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 Opeartion 완료 시점과 Future 완료 시점이 다르다는 점과 관련이 있습니다.

전체 응답을 받으면 Operation은 완료되지만, Future는 디코딩을 기다리거나 디코딩하는 동안 아직 미완료 상태입니다.

기존에는 op.cancel()이 호출하는 cancel 콜백을 통해 Future를 취소했지만, 이미 완료된 Operation은 op.cancel()이 false를 반환하고 callback도 호출하지 않아 Future의 상태가 isDone() == false 인데도 디코딩 중인 Future를 취소할 수 없었습니다.

따라서 응답을 받은 뒤에는 super.cancel()로 Future를 직접 취소하고, 디코딩 전이면 디코딩을 건너뛰고 디코딩 중이면 해당 결과를 버리는 방식으로 수정했습니다.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

결국 다음 사항을 위해 상태를 관리하고 cancel 처리를 수정해야 하는 거로 이해했습니다.

  • 타임아웃 처리중 cancel 요청이 들어오는 경우 무시
  • 디코딩중 cancel 요청이 들어오는 경우 취소 처리

Comment thread src/main/java/net/spy/memcached/v2/ArcusFutureImpl.java
MemcachedConnection.opTimedOut(op);
op.cancel("by operation timeout");

ArcusExecutors.COMPLETION_EXECUTOR.execute(() -> super.completeExceptionally(exception));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super.completeExceptionally 호출은 금방끝나는 작업이라 completion 스레드에 맡기지 않아도 될 것 같은데,
이렇게 구성한 이유가 무엇인가요?

@f1v3-dev f1v3-dev Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super.completeExceptionally()는 Future의 상태만 바꾸는 것이 아니라, 사용자가 등록한 exceptionally()나 whenComplete() 같은 후속 작업을 호출한 스레드에서 바로 실행할 수 있습니다. 그래서 호출이 빨리 끝난다고 보장하기는 어려워 보입니다.

또한 TIMEOUT_SCHEDULER는 단일 스레드에서 모든 Operation의 timeout()을 실행하므로, 여기서 사용자 코드가 오래 실행되면 다른 Operation의 timeout 처리까지 지연됩니다.

이를 막기 위해 Future 예외 완료를 COMPLETION_EXECUTOR에 위임하도록 구성했습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

물론, COMPLETION_EXECUTOR 가 예외 처리를 하게 되어 디코딩 작업이 밀릴 순 있으나, 이는 추후에 고려하여 설계하는 방향으로 진행하고자 합니다.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completablefuture 사용하는 경우 일반적으로 후속 작업들을 논블로킹으로 처리하도록 구현하기 때문에 IO Thread가 직접 수행해도 됩니다.
블로킹 작업을 처리하는 경우 별도 스레드풀에 넘기도록 가이드하는 건 필요할 것 같습니다.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기만 다시 논의해서 진행하면 될 것 같습니다..!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 IO Thread는 여기서 timeout() 메서드를 실행하는 TIMEOUT_SCHEDULER 를 의미하신게 맞을까요?

해당 scheduler는 단일 스레드라서, completeExceptionally() 호출 중 사용자 콜백(whenComplete, exceptionally, handle 등)이 오래 실행되면 다른 Operation의 timeout 처리까지 지연될 수 있다고 보여집니다.

따라서 예외 완료 처리는 별도 스레드에 넘기는 편이 좋다고 생각했는데, 이 부분은 어떻게 보시나요?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finishResponse 메서드 호출하는 MemcachedClient 의 IO 스레드를 의미했습니다.

용자 콜백(whenComplete, exceptionally, handle 등)이 오래 실행되면 다른 Operation의 timeout 처리까지 지연될 수 있도록 작성하는 패턴 자체가 논블로킹 애플리케이션 구현 시에 안티 패턴이기 때문에, 그렇게 작성하지 않도록 경고하는 정도로도 충분해보입니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 내용을 아래와 같이 이해했는데 맞는지 확인 부탁드립니다.

timeout() 에서는 super.completeExceptionally() 를 직접 호출하고, 오래 걸리거나 블로킹하는 사용자 후속 작업은 whenCompleteAsync(..., userExecutor) 처럼 사용자가 별도 executor에서 실행하도록 한다.

이 방향이 맞다면 완료 위임을 제거하고, Javadoc에 해당 가이드를 반영하겠습니다.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다. 일반적으로 CompletableFuture 사용하는 패턴에서는 최대한 블로킹되지 않는 형태로 코드를 작성할 것이고 블로킹 메서드를 어쩔 수 없이 사용해야 하는 경우에는 async가 붙은 비동기 메서드를 통해 별도 스레드에 위임하도록 작성할 것입니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신 내용 반영하였으며 ArcusFuture Javadoc에 아래와 같이 설명을 추가했습니다.

Non-async dependent stages may run on Arcus internal threads, so their actions must be short and non-blocking. For blocking or long-running work, use async methods with an application-provided executor.

@f1v3-dev
f1v3-dev force-pushed the feat/v2-op-timeout branch from 2fa366b to eafc550 Compare October 8, 2026 00:36
@oliviarla
oliviarla requested a review from jhpark816 October 14, 2026 08:57

@jhpark816 jhpark816 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일부 리뷰

// A response or cancellation may have come first while scheduling.
if (state.get() != WAITING_RESPONSE) {
cancelTimeoutTask();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WAITING_RESPONSE 상태에서 scheduleTimeout() 호출합니다.

  • scheduleTimeout() 메서드 시작과 마지막에 WAITING_RESPONSE 상태를 검사하여 얻는 이득은 무엇인가요?
  • 만약 이러한 검사를 생략할 경우, 어떤 문제가 생기나요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scheduleTimeout() 메서드 시작과 마지막에 WAITING_RESPONSE 상태를 검사하여 얻는 이득은 무엇인가요?

가능성은 적지만, addOp() 내부의 아래 경우에는 Operation이 바로 취소되어 scheduleTimeout() 호출 시 WAITING_RESPONSE 상태가 아닌 DECIDED 상태로 진입할 수 있습니다.

  • node 없음
  • 인증 실패 (SASL 사용 시)
  • node inactive && FailureMode.Cancel

시작 검사는 이 경우 timeout task 등록을 건너뛰고, 마지막 검사는 등록 도중 응답이나 취소가 먼저 도착한 경우 등록한 task를 해제합니다.

만약 이러한 검사를 생략할 경우, 어떤 문제가 생기나요?

두 검사를 생략해도 만료 시 timeout() 메서드가 동작할 뿐이며, 아래 로직으로 인해 결과에는 문제가 발생하지 않습니다.

if (!state.compareAndSet(WAITING_RESPONSE, DECIDED)) {
  return;
}

위 경우 task가 opTimeout 동안 scheduler Queue에 남는 정도이고, 시간이 지나면 정리되므로 이 정도는 감수할 수 있다는 조건이라면 제거해도 무방할 것으로 보입니다.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addOp() 내부에서 cancel 될 수 있으므로,
scheduleTimeout() 시작 시의 검사는 유용해 보입니다.

scheduleTimeout() 마지막 검사는 생략해도 될 것 같습니다.

  • timeoutTask 등록 후에 cancel할 경우는 cancelTimeoutTask() 호출함
  • 시작 시 검사가 있으므로, cancel된 연산에 대해 timeoutTask 등록할 가능성은 매우 낮음.

} catch (Exception e) {
failResponse(e);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decodeResponse()는 RESPONSE_RECEIVED 상태에서 수행합니다.
시작 부분에 RESPONSE_RECEIVED 상태 검사를 생략해도 될 것 같은 데,
생략하면 어떤 문제가 생기나요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재는 응답 수신 이후에도 cancel()을 허용하고 있어, 디코딩 전에 cancel()이 호출되면 DECIDED 상태로 해당 메서드에 진입할 수 있습니다. 시작 검사 부분은 이러한 케이스에서 디코딩을 건너뛰기 위함이었습니다.

확인해보니, 생략하더라도 디코딩 이후 아래 로직으로 인해 결과가 버려지므로 문제가 발생하지는 않네요

private void finishResponse(T value) {
  if (state.compareAndSet(RESPONSE_RECEIVED, DECIDED)) {
    super.complete(value);
  }
}

다만, 응답 수신 이후의 cancel() 허용 여부는 따로 정해야할 것 같습니다.

  • 허용하지 않는 경우: v1과 동일하게 원격 요청에만 cancel이 적용되고, RESPONSE_RECEIVED와 DECIDED를 합쳐 상태를 단순화할 수 있음.
  • 허용하는 경우: CompletableFuture.cancel()은 완료 전이면 취소에 성공하는 계약이고, 타 자바 클라이언트(Lettcue, MongoDB)도 응답 수신 여부와 관계없이 완료 전이면 취소에 성공함.

어느 쪽이든 해당 검사는 필요하지 않으므로 제거한 뒤 cancel 허용 범위는 별도로 논의하면 좋을 것 같습니다.

@jhpark816 jhpark816 Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 Operation 코드를 보면 callbacked 값을 통해 complete or cancel 중의 하나로 처리됩니다.

  public final boolean cancel(String cause) {
    if (callbacked.compareAndSet(false, true)) {
      cancelled = true;
      if (handlingNode != null) {
        cause += " @ " + handlingNode.getNodeName();
      }
      cancelCause = "Cancelled (" + cause + ")";
      callback.receivedStatus(CANCELLED);
      callback.complete();
      return true;
    }
    return false;
  }

  protected final void complete(OperationStatus status) {
    boolean needComplete = callbacked.compareAndSet(false, true);
    callback.receivedStatus(status);
    transitionState(OperationState.COMPLETE);
    if (needComplete) {
      callback.complete();
    }
  }

따라서, Future 수준에서 state를 두고 complete or cancel 제어할 필요성은 없어 보입니다.

참고로, v2에서 timeout 발생 시에
아래 코드와 같이 op.cancel() 호출하면 되지 않나 생각합니다.
super.completeExceptionally(exception)는 호출하지 않아도 될 것 같습니다.

 private void timeout(long timeoutMillis) {
    if (this.isDone()) {
      return false;
    }
 
   OperationTimeoutException exception = xxx;

    return op.cancel("by operation timeout." + exception);
}

기존의 complete, cancel 처리 틀을 가급적 그대로 이용하여도 될 것 같습니다.
검토 바랍니다.

} catch (Exception e) {
this.completeExceptionally(e);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decodeResponse() 메서드에서 초기 상태 검사를 생략한다면,
위 코드에서 아래 코드 부분만 finishResponse(), failResponse()로 변경하면 될 것 같습니다.

  • this.complete()
  • this.completeExceptionally()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@f1v3-dev
질문입니다.
cancel 시에 decoding 작업을 취소하는 로직은 없죠?
즉, decoding 작업이 요청되면, 무조건 수행이 완료되죠?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

즉, decoding 작업이 요청되면, 무조건 수행이 완료되죠?

맞습니다. 이미 시작된 디코딩을 중단하는 로직은 없습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants