Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 84 additions & 5 deletions shortcuts/wiki/wiki_node_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

import (
"context"
"errors"
"fmt"
"io"
"strings"
"time"

"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
Expand All @@ -24,6 +27,16 @@
wikiResolvedByMyLibrary = "my_library"
)

const (
// wikiNodeCreateMaxRetries is the maximum number of retry attempts after
// the initial request when the API returns lock contention (code 131009).
wikiNodeCreateMaxRetries = 2

// wikiNodeCreateRetryBaseDelay is the initial backoff delay for lock
// contention retries. Subsequent retries double the delay (250ms, 500ms).
wikiNodeCreateRetryBaseDelay = 250 * time.Millisecond
)
Comment thread
fangshuyu-768 marked this conversation as resolved.

var wikiObjectTypes = []string{
"sheet",
"mindnote",
Expand Down Expand Up @@ -68,7 +81,7 @@
spec := readWikiNodeCreateSpec(runtime)

fmt.Fprintf(runtime.IO().ErrOut, "Creating wiki node...\n")
execution, err := runWikiNodeCreate(ctx, wikiNodeCreateAPI{runtime: runtime}, runtime.As(), spec)
execution, err := runWikiNodeCreate(ctx, wikiNodeCreateAPI{runtime: runtime}, runtime.As(), spec, runtime.IO().ErrOut)
if err != nil {
return err
}
Expand Down Expand Up @@ -288,15 +301,37 @@
return spec.SpaceID == "" || spec.SpaceID == wikiMyLibrarySpaceID
}

func runWikiNodeCreate(ctx context.Context, client wikiNodeCreateClient, identity core.Identity, spec wikiNodeCreateSpec) (*wikiNodeCreateExecution, error) {
func runWikiNodeCreate(ctx context.Context, client wikiNodeCreateClient, identity core.Identity, spec wikiNodeCreateSpec, errOut io.Writer) (*wikiNodeCreateExecution, error) {
resolvedSpace, err := resolveWikiNodeCreateSpace(ctx, client, identity, spec)
if err != nil {
return nil, err
}

node, err := client.CreateNode(ctx, resolvedSpace.SpaceID, spec)
if err != nil {
return nil, err
var (
node *wikiNodeRecord
lastErr error
)
for attempt := 0; attempt <= wikiNodeCreateMaxRetries; attempt++ {
if attempt > 0 {
delay := wikiNodeCreateRetryBaseDelay << uint(attempt-1)
fmt.Fprintf(errOut, "Wiki node create encountered lock contention, retrying (attempt %d/%d) in %v...\n", attempt, wikiNodeCreateMaxRetries, delay)
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}

node, lastErr = client.CreateNode(ctx, resolvedSpace.SpaceID, spec)
if lastErr == nil {
break
}
if !isWikiNodeLockContention(lastErr) {
return nil, lastErr
}
}
if lastErr != nil {
return nil, wrapWikiNodeCreateRetryError(lastErr)
}
if node == nil {
return nil, output.Errorf(output.ExitAPI, "api_error", "wiki node create returned no node")
Expand All @@ -308,6 +343,50 @@
}, nil
}

// isWikiNodeLockContention returns true if the error is a Lark API error with
// code 131009 (wiki node lock contention), which is retryable with backoff.
func isWikiNodeLockContention(err error) bool {
var exitErr *output.ExitError
if !errors.As(err, &exitErr) || exitErr.Detail == nil {
return false

Check warning on line 351 in shortcuts/wiki/wiki_node_create.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/wiki/wiki_node_create.go#L351

Added line #L351 was not covered by tests
}
return exitErr.Detail.Code == output.LarkErrWikiLockContention
}

// wrapWikiNodeCreateRetryError appends a retry-exhaustion hint to the original
// API error. It builds the ExitError by hand (instead of using ErrWithHint) so
// the original Lark error code survives in the envelope.
func wrapWikiNodeCreateRetryError(err error) error {
if err == nil {
return nil

Check warning on line 361 in shortcuts/wiki/wiki_node_create.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/wiki/wiki_node_create.go#L361

Added line #L361 was not covered by tests
}
var exitErr *output.ExitError
if !errors.As(err, &exitErr) || exitErr.Detail == nil {
return err

Check warning on line 365 in shortcuts/wiki/wiki_node_create.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/wiki/wiki_node_create.go#L365

Added line #L365 was not covered by tests
}
hint := fmt.Sprintf(
"wiki node create failed after %d retries due to lock contention; try again later or reduce concurrent node creations under the same parent",
wikiNodeCreateMaxRetries,
)
if existing := strings.TrimSpace(exitErr.Detail.Hint); existing != "" {
hint = existing + "\n" + hint
}
return &output.ExitError{
Code: exitErr.Code,
Detail: &output.ErrDetail{
Type: exitErr.Detail.Type,
Code: exitErr.Detail.Code,
Message: exitErr.Detail.Message,
Hint: hint,
ConsoleURL: exitErr.Detail.ConsoleURL,
Risk: exitErr.Detail.Risk,
Detail: exitErr.Detail.Detail,
},
Err: exitErr.Err,
Raw: exitErr.Raw,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// resolveWikiNodeCreateSpace applies the shortcut's precedence rules:
// explicit space ID wins, then parent-node inference, then my_library fallback.
func resolveWikiNodeCreateSpace(ctx context.Context, client wikiNodeCreateClient, identity core.Identity, spec wikiNodeCreateSpec) (wikiResolvedSpace, error) {
Expand Down
Loading
Loading