Skip to content

Commit fefe32c

Browse files
authored
Add support for operation token and deprecate operation ID (#38)
- Also fix docstring for handler error types - Also mark all exported functions and structs in completion.go as experimental - those need to be redone to properly rehydrate errors See also nexus-rpc/api#16
1 parent 24d8574 commit fefe32c

15 files changed

+339
-148
lines changed

Diff for: nexus/api.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,14 @@ const (
2929
headerRetryable = "nexus-request-retryable"
3030
// HeaderOperationID is the unique ID returned by the StartOperation response for async operations.
3131
// Must be set on callback headers to support completing operations before the start response is received.
32+
//
33+
// Deprecated: Use HeaderOperationToken instead.
3234
HeaderOperationID = "nexus-operation-id"
3335

36+
// HeaderOperationToken is the unique token returned by the StartOperation response for async operations.
37+
// Must be set on callback headers to support completing operations before the start response is received.
38+
HeaderOperationToken = "nexus-operation-token"
39+
3440
// HeaderRequestTimeout is the total time to complete a Nexus HTTP request.
3541
HeaderRequestTimeout = "request-timeout"
3642
// HeaderOperationTimeout is the total time to complete a Nexus operation.
@@ -184,13 +190,13 @@ const (
184190
// The caller does not have permission to execute the specified operation. Clients should not retry this
185191
// request unless advised otherwise.
186192
HandlerErrorTypeUnauthorized HandlerErrorType = "UNAUTHORIZED"
187-
// The requested resource could not be found but may be available in the future. Subsequent requests by the
188-
// client are permissible but not advised.
193+
// The requested resource could not be found but may be available in the future. Clients should not retry this
194+
// request unless advised otherwise.
189195
HandlerErrorTypeNotFound HandlerErrorType = "NOT_FOUND"
190196
// Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of
191197
// space. Subsequent requests by the client are permissible.
192198
HandlerErrorTypeResourceExhausted HandlerErrorType = "RESOURCE_EXHAUSTED"
193-
// An internal error occured. Clients should not retry this request unless advised otherwise.
199+
// An internal error occured. Subsequent requests by the client are permissible.
194200
HandlerErrorTypeInternal HandlerErrorType = "INTERNAL"
195201
// The server either does not recognize the request method, or it lacks the ability to fulfill the request.
196202
// Clients should not retry this request unless advised otherwise.
@@ -287,7 +293,11 @@ var ErrOperationStillRunning = errors.New("operation still running")
287293
// OperationInfo conveys information about an operation.
288294
type OperationInfo struct {
289295
// ID of the operation.
296+
//
297+
// Deprecated: Use Token instead.
290298
ID string `json:"id"`
299+
// Token for the operation.
300+
Token string `json:"token"`
291301
// State of the operation.
292302
State OperationState `json:"state"`
293303
}

Diff for: nexus/cancel_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ type asyncWithCancelHandler struct {
1515

1616
func (h *asyncWithCancelHandler) StartOperation(ctx context.Context, service, operation string, input *LazyValue, options StartOperationOptions) (HandlerStartOperationResult[any], error) {
1717
return &HandlerStartOperationResultAsync{
18-
OperationID: "a/sync",
18+
OperationToken: "a/sync",
1919
}, nil
2020
}
2121

22-
func (h *asyncWithCancelHandler) CancelOperation(ctx context.Context, service, operation, operationID string, options CancelOperationOptions) error {
22+
func (h *asyncWithCancelHandler) CancelOperation(ctx context.Context, service, operation, token string, options CancelOperationOptions) error {
2323
if service != testService {
2424
return HandlerErrorf(HandlerErrorTypeBadRequest, "unexpected service: %s", service)
2525
}
2626
if operation != "f/o/o" {
2727
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected operation to be 'foo', got: %s", operation)
2828
}
29-
if operationID != "a/sync" {
30-
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected operation ID to be 'async', got: %s", operationID)
29+
if token != "a/sync" {
30+
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected operation ID to be 'async', got: %s", token)
3131
}
3232
if h.expectHeader && options.Header.Get("foo") != "bar" {
3333
return HandlerErrorf(HandlerErrorTypeBadRequest, "invalid 'foo' request header")
@@ -69,11 +69,11 @@ type echoTimeoutAsyncWithCancelHandler struct {
6969

7070
func (h *echoTimeoutAsyncWithCancelHandler) StartOperation(ctx context.Context, service, operation string, input *LazyValue, options StartOperationOptions) (HandlerStartOperationResult[any], error) {
7171
return &HandlerStartOperationResultAsync{
72-
OperationID: "timeout",
72+
OperationToken: "timeout",
7373
}, nil
7474
}
7575

76-
func (h *echoTimeoutAsyncWithCancelHandler) CancelOperation(ctx context.Context, service, operation, operationID string, options CancelOperationOptions) error {
76+
func (h *echoTimeoutAsyncWithCancelHandler) CancelOperation(ctx context.Context, service, operation, token string, options CancelOperationOptions) error {
7777
deadline, set := ctx.Deadline()
7878
if h.expectedTimeout > 0 && !set {
7979
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected operation to have timeout set but context has no deadline")

Diff for: nexus/client.go

+23-13
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ type HTTPClientOptions struct {
3333
// A [FailureConverter] to convert a [Failure] instance to and from an [error]. Defaults to
3434
// [DefaultFailureConverter].
3535
FailureConverter FailureConverter
36+
// UseOperationID instructs the client to use an older format of the protocol where operation ID is sent
37+
// as part of the URL path.
38+
// This flag will be removed in a future release.
39+
//
40+
// NOTE: Experimental
41+
UseOperationID bool
3642
}
3743

3844
// User-Agent header set on HTTP requests.
@@ -42,7 +48,7 @@ const headerUserAgent = "User-Agent"
4248

4349
var errEmptyOperationName = errors.New("empty operation name")
4450

45-
var errEmptyOperationID = errors.New("empty operation ID")
51+
var errEmptyOperationToken = errors.New("empty operation token")
4652

4753
var errOperationWaitTimeout = errors.New("operation wait timeout")
4854

@@ -268,13 +274,16 @@ func (c *HTTPClient) StartOperation(
268274
if info.State != OperationStateRunning {
269275
return nil, newUnexpectedResponseError(fmt.Sprintf("invalid operation state in response info: %q", info.State), response, body)
270276
}
277+
if info.Token == "" && info.ID != "" {
278+
info.Token = info.ID
279+
}
280+
handle, err := c.NewHandle(operation, info.Token)
281+
if err != nil {
282+
return nil, newUnexpectedResponseError("empty operation token in response", response, body)
283+
}
271284
return &ClientStartOperationResult[*LazyValue]{
272-
Pending: &OperationHandle[*LazyValue]{
273-
Operation: operation,
274-
ID: info.ID,
275-
client: c,
276-
},
277-
Links: links,
285+
Pending: handle,
286+
Links: links,
278287
}, nil
279288
case statusOperationFailed:
280289
state, err := getUnsuccessfulStateFromHeader(response, body)
@@ -366,24 +375,25 @@ func (c *HTTPClient) ExecuteOperation(ctx context.Context, operation string, inp
366375
return handle.GetResult(ctx, gro)
367376
}
368377

369-
// NewHandle gets a handle to an asynchronous operation by name and ID.
378+
// NewHandle gets a handle to an asynchronous operation by name and token.
370379
// Does not incur a trip to the server.
371-
// Fails if provided an empty operation or ID.
372-
func (c *HTTPClient) NewHandle(operation string, operationID string) (*OperationHandle[*LazyValue], error) {
380+
// Fails if provided an empty operation or token.
381+
func (c *HTTPClient) NewHandle(operation string, token string) (*OperationHandle[*LazyValue], error) {
373382
var es []error
374383
if operation == "" {
375384
es = append(es, errEmptyOperationName)
376385
}
377-
if operationID == "" {
378-
es = append(es, errEmptyOperationID)
386+
if token == "" {
387+
es = append(es, errEmptyOperationToken)
379388
}
380389
if len(es) > 0 {
381390
return nil, errors.Join(es...)
382391
}
383392
return &OperationHandle[*LazyValue]{
384393
client: c,
385394
Operation: operation,
386-
ID: operationID,
395+
ID: token, // Duplicate token as ID for the deprecation period.
396+
Token: token,
387397
}, nil
388398
}
389399

0 commit comments

Comments
 (0)