feat: deliver authenticated task APIs, SDK and P1 regression
This commit is contained in:
@@ -12,12 +12,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Options currently configures bootstrap health calls. Task, Worker and workflow
|
||||
// clients will be added alongside their corresponding server endpoints.
|
||||
type Options struct{ HTTPClient *http.Client }
|
||||
// Options 配置传输与命名空间凭据;健康查询不使用凭据。
|
||||
type Options struct {
|
||||
HTTPClient *http.Client
|
||||
Token string
|
||||
}
|
||||
type Client struct {
|
||||
baseURL string
|
||||
http *http.Client
|
||||
token string
|
||||
}
|
||||
type Health struct {
|
||||
Status string `json:"status"`
|
||||
@@ -34,6 +37,9 @@ func NewClient(baseURL string, opts Options) (*Client, error) {
|
||||
if err != nil || u == nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
|
||||
return nil, errors.New("Orbit base URL must be absolute HTTP(S), without credentials, query or fragment")
|
||||
}
|
||||
if len(opts.Token) > 1024 || strings.ContainsAny(opts.Token, " \t\r\n") {
|
||||
return nil, errors.New("invalid Orbit token")
|
||||
}
|
||||
client := opts.HTTPClient
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 10 * time.Second}
|
||||
@@ -41,7 +47,7 @@ func NewClient(baseURL string, opts Options) (*Client, error) {
|
||||
clone := *client
|
||||
// Do not silently forward requests to another origin through redirects.
|
||||
clone.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
|
||||
return &Client{baseURL: strings.TrimRight(u.String(), "/"), http: &clone}, nil
|
||||
return &Client{baseURL: strings.TrimRight(u.String(), "/"), http: &clone, token: opts.Token}, nil
|
||||
}
|
||||
func (c *Client) Liveness(ctx context.Context) (Health, error) { return c.health(ctx, "/health/live") }
|
||||
func (c *Client) Readiness(ctx context.Context) (Health, error) {
|
||||
|
||||
87
sdk/go/examples/tasks/main.go
Normal file
87
sdk/go/examples/tasks/main.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// tasks 演示受理、重复提交、查询、分页与取消;此示例不启动业务 Worker。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
func run() error {
|
||||
token, queue := os.Getenv("ORBIT_TOKEN"), os.Getenv("ORBIT_QUEUE_ID")
|
||||
if path := os.Getenv("ORBIT_CREDENTIALS_FILE"); path != "" {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read private credentials: %w", err)
|
||||
}
|
||||
var credentials struct {
|
||||
Token string `json:"token"`
|
||||
QueueID string `json:"queue_id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &credentials); err != nil {
|
||||
return fmt.Errorf("invalid credentials file")
|
||||
}
|
||||
if token == "" {
|
||||
token = credentials.Token
|
||||
}
|
||||
if queue == "" {
|
||||
queue = credentials.QueueID
|
||||
}
|
||||
}
|
||||
client, err := orbit.NewClient(os.Getenv("ORBIT_URL"), orbit.Options{Token: token})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if queue == "" {
|
||||
return fmt.Errorf("ORBIT_QUEUE_ID is required")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
var operation [16]byte
|
||||
if _, err := rand.Read(operation[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
key := "example-" + hex.EncodeToString(operation[:])
|
||||
// 生产接入应先在业务库保存稳定键;本示例的两次请求共用同一个键。
|
||||
in := orbit.SubmitTaskRequest{QueueID: queue, Handler: "file.convert", HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{"source_ref":"example/input.txt","format":"pdf","sequence":9007199254740993}`)}
|
||||
first, err := client.SubmitTask(ctx, key, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
again, err := client.SubmitTask(ctx, key, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if first.Task.ID != again.Task.ID || !again.Deduplicated {
|
||||
return fmt.Errorf("duplicate submission did not return the original task")
|
||||
}
|
||||
current, err := client.GetTask(ctx, first.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
page, err := client.ListTasks(ctx, orbit.ListTasksRequest{QueueID: queue, Limit: 10})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cancelled, err := client.CancelTask(ctx, current.ID, key+"-cancel", orbit.CancelTaskRequest{Reason: "finish task acceptance example", ExpectedRevision: current.Revision})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("task=%s duplicate=%t listed=%d final_state=%s revision=%d\n", current.ID, again.Deduplicated, len(page.Items), cancelled.Task.State, cancelled.Task.Revision)
|
||||
if cancelled.Task.State != orbit.TaskCancelled {
|
||||
return fmt.Errorf("expected CANCELLED")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
161
sdk/go/tasks.go
Normal file
161
sdk/go/tasks.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package orbit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// APIError 提供可分支处理的错误码和请求追踪标识,不包含内部数据库错误。
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Code string
|
||||
Message string
|
||||
RequestID string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("orbit %s (HTTP %d, request %s): %s", e.Code, e.StatusCode, e.RequestID, e.Message)
|
||||
}
|
||||
func (e *APIError) Unwrap() error { return &HTTPError{StatusCode: e.StatusCode} }
|
||||
|
||||
// SubmitTask 使用调用方持久化的幂等键;SDK 不自动生成新键或自动重试写请求。
|
||||
func (c *Client) SubmitTask(ctx context.Context, key string, in SubmitTaskRequest) (SubmitTaskResult, error) {
|
||||
var out SubmitTaskResult
|
||||
err := c.call(ctx, "POST", "/v1/tasks", key, in, &out)
|
||||
return out, err
|
||||
}
|
||||
func (c *Client) GetTask(ctx context.Context, id string) (Task, error) {
|
||||
var out Task
|
||||
if !validID(id) {
|
||||
return out, errors.New("invalid task id")
|
||||
}
|
||||
err := c.call(ctx, "GET", "/v1/tasks/"+id, "", nil, &out)
|
||||
return out, err
|
||||
}
|
||||
func (c *Client) ListTasks(ctx context.Context, in ListTasksRequest) (TaskPage, error) {
|
||||
var out TaskPage
|
||||
q := url.Values{}
|
||||
q.Set("queue_id", in.QueueID)
|
||||
if in.State != "" {
|
||||
q.Set("state", string(in.State))
|
||||
}
|
||||
if in.Limit != 0 {
|
||||
q.Set("limit", strconv.Itoa(in.Limit))
|
||||
}
|
||||
if in.Cursor != "" {
|
||||
q.Set("cursor", in.Cursor)
|
||||
}
|
||||
err := c.call(ctx, "GET", "/v1/tasks?"+q.Encode(), "", nil, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// CancelTask 当前只取消 WAITING 状态且不归属流程的任务;重复请求使用原键与原版本。
|
||||
func (c *Client) CancelTask(ctx context.Context, id, key string, in CancelTaskRequest) (CancelTaskResult, error) {
|
||||
var out CancelTaskResult
|
||||
if !validID(id) {
|
||||
return out, errors.New("invalid task id")
|
||||
}
|
||||
err := c.call(ctx, "POST", "/v1/tasks/"+id+":cancel", key, in, &out)
|
||||
return out, err
|
||||
}
|
||||
func (c *Client) CreateQueue(ctx context.Context, key string, in CreateQueueRequest) (CreateQueueResult, error) {
|
||||
var out CreateQueueResult
|
||||
err := c.call(ctx, "POST", "/v1/queues", key, in, &out)
|
||||
return out, err
|
||||
}
|
||||
func (c *Client) GetQueue(ctx context.Context, id string) (Queue, error) {
|
||||
var out Queue
|
||||
if !validID(id) {
|
||||
return out, errors.New("invalid queue id")
|
||||
}
|
||||
err := c.call(ctx, "GET", "/v1/queues/"+id, "", nil, &out)
|
||||
return out, err
|
||||
}
|
||||
func validID(id string) bool {
|
||||
if len(id) != 32 {
|
||||
return false
|
||||
}
|
||||
for _, ch := range id {
|
||||
if !(ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'f') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func (c *Client) call(ctx context.Context, method, path, key string, in, out any) error {
|
||||
if c.token == "" {
|
||||
return errors.New("Orbit token is required for business APIs")
|
||||
}
|
||||
if method != "GET" {
|
||||
if len(key) < 1 || len(key) > 128 {
|
||||
return errors.New("a stable Idempotency-Key of 1 to 128 ASCII characters is required")
|
||||
}
|
||||
for _, ch := range key {
|
||||
if ch < 33 || ch > 126 {
|
||||
return errors.New("Idempotency-Key must not contain spaces or control characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
var body []byte
|
||||
var err error
|
||||
if in != nil {
|
||||
body, err = json.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(body) > 512<<10 {
|
||||
return errors.New("Orbit request exceeds 512 KiB")
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
if in != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if key != "" {
|
||||
req.Header.Set("Idempotency-Key", key)
|
||||
}
|
||||
response, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
const maxResponse = 1 << 20
|
||||
raw, err := io.ReadAll(io.LimitReader(response.Body, maxResponse+1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(raw) > maxResponse {
|
||||
return errors.New("Orbit response exceeds 1 MiB")
|
||||
}
|
||||
var envelope struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Error *struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
decoded := json.Unmarshal(raw, &envelope)
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
if decoded == nil && envelope.Error != nil && len(envelope.Error.Code) <= 64 && len(envelope.Error.Message) <= 1024 && validID(envelope.RequestID) {
|
||||
return &APIError{response.StatusCode, envelope.Error.Code, envelope.Error.Message, envelope.RequestID}
|
||||
}
|
||||
return &HTTPError{StatusCode: response.StatusCode}
|
||||
}
|
||||
if decoded != nil || len(envelope.Data) == 0 || string(envelope.Data) == "null" || envelope.Error != nil || !validID(envelope.RequestID) {
|
||||
return errors.New("invalid Orbit response envelope")
|
||||
}
|
||||
return json.Unmarshal(envelope.Data, out)
|
||||
}
|
||||
72
sdk/go/tasks_test.go
Normal file
72
sdk/go/tasks_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package orbit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTaskClientContractAndNoSilentRetry(t *testing.T) {
|
||||
const id = "11111111111111111111111111111111"
|
||||
var calls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
if r.Header.Get("Authorization") != "Bearer example-token" || r.Header.Get("Idempotency-Key") != "stable" {
|
||||
t.Error("lost authorization or identity")
|
||||
}
|
||||
raw, _ := io.ReadAll(r.Body)
|
||||
if !strings.Contains(string(raw), "9007199254740993") {
|
||||
t.Error("payload lost integer precision")
|
||||
}
|
||||
if calls.Load() == 1 {
|
||||
w.WriteHeader(503)
|
||||
_, _ = io.WriteString(w, `{"request_id":"`+id+`","error":{"code":"UNAVAILABLE","message":"retry original identity"}}`)
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"request_id":"`+id+`","data":{"deduplicated":true,"task":{"id":"`+id+`","revision":"9007199254740993","payload":{"sequence":9007199254740993}}}}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, err := NewClient(srv.URL, Options{Token: "example-token"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := SubmitTaskRequest{QueueID: id, Payload: json.RawMessage(`{"sequence":9007199254740993}`)}
|
||||
_, err = client.SubmitTask(context.Background(), "stable", request)
|
||||
var apiErr *APIError
|
||||
var httpErr *HTTPError
|
||||
if !errors.As(err, &apiErr) || apiErr.Code != "UNAVAILABLE" || apiErr.RequestID != id || !errors.As(err, &httpErr) || httpErr.StatusCode != 503 || calls.Load() != 1 {
|
||||
t.Fatalf("error/retry contract: %v, calls=%d", err, calls.Load())
|
||||
}
|
||||
out, err := client.SubmitTask(context.Background(), "stable", request)
|
||||
if err != nil || !out.Deduplicated || out.Task.Revision != 9007199254740993 || string(out.Task.Payload) != `{"sequence":9007199254740993}` {
|
||||
t.Fatalf("response: %+v %v", out, err)
|
||||
}
|
||||
}
|
||||
func TestBusinessClientRedirectAndPathBoundaries(t *testing.T) {
|
||||
var reached atomic.Bool
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached.Store(true) }))
|
||||
defer target.Close()
|
||||
source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL, 307) }))
|
||||
defer source.Close()
|
||||
client, _ := NewClient(source.URL, Options{Token: "example-token"})
|
||||
_, err := client.GetTask(context.Background(), strings.Repeat("1", 32))
|
||||
var failure *HTTPError
|
||||
if !errors.As(err, &failure) || failure.StatusCode != 307 || reached.Load() {
|
||||
t.Fatalf("redirect leaked request: %v", err)
|
||||
}
|
||||
for _, id := range []string{"../queues", "abc?token=bad", strings.Repeat("A", 32)} {
|
||||
if _, err := client.GetTask(context.Background(), id); err == nil {
|
||||
t.Fatal("accepted unsafe ID")
|
||||
}
|
||||
}
|
||||
missing, _ := NewClient(source.URL, Options{})
|
||||
if _, err := missing.GetQueue(context.Background(), strings.Repeat("1", 32)); err == nil {
|
||||
t.Fatal("missing token allowed")
|
||||
}
|
||||
}
|
||||
200
sdk/go/types.go
Normal file
200
sdk/go/types.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Package orbit provides the standalone Orbit Go SDK.
|
||||
package orbit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SubmitTaskRequest 是任务受理命令;可变配置在受理时固定为快照。
|
||||
type SubmitTaskRequest struct {
|
||||
// QueueID 目标队列,必须位于凭据所属命名空间。
|
||||
QueueID string `json:"queue_id"`
|
||||
// Handler 业务 Worker 的处理器名称。
|
||||
Handler string `json:"handler"`
|
||||
// HandlerVersion 固定处理器版本。
|
||||
HandlerVersion string `json:"handler_version"`
|
||||
// PayloadVersion 输入参数结构版本。
|
||||
PayloadVersion string `json:"payload_version"`
|
||||
// Payload 业务无关 JSON 文档,最大 256 KiB,保留精确数值文本。
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
// BusinessKey 可选关联键,不提供业务效果幂等保证。
|
||||
BusinessKey string `json:"business_key,omitempty"`
|
||||
// AvailableAt 最早执行时间;为空表示受理后即可进入派发范围。
|
||||
AvailableAt *time.Time `json:"available_at,omitempty"`
|
||||
// StartDeadlineAt 最晚开始时间;为空表示使用任务总截止时间。
|
||||
StartDeadlineAt *time.Time `json:"start_deadline_at,omitempty"`
|
||||
// TaskTimeoutMS 从受理时计算的总时限,单位毫秒。
|
||||
TaskTimeoutMS uint64 `json:"task_timeout_ms"`
|
||||
// RecoverySafety 外部效果未知时的恢复安全等级。
|
||||
RecoverySafety RecoverySafety `json:"recovery_safety"`
|
||||
// Retry 每次执行的有限尝试策略,包含首次尝试。
|
||||
Retry RetryPolicy `json:"retry"`
|
||||
}
|
||||
|
||||
// Task 是对外任务快照,不包含 Worker 令牌或凭据摘要。
|
||||
type Task struct {
|
||||
// ID 资源标识,32 位小写十六进制字符串。
|
||||
ID string `json:"id"`
|
||||
// NamespaceID 命名空间标识,由凭据确定隔离范围。
|
||||
NamespaceID string `json:"namespace_id"`
|
||||
// QueueID 队列标识,必须属于当前命名空间。
|
||||
QueueID string `json:"queue_id"`
|
||||
// State 当前任务状态;终态不会被重复请求重开。
|
||||
State TaskState `json:"state"`
|
||||
// Revision 并发版本,JSON 使用十进制字符串避免跨语言精度丢失。
|
||||
Revision uint64 `json:"revision,string"`
|
||||
// Handler Worker 处理器名称。
|
||||
Handler string `json:"handler"`
|
||||
// HandlerVersion 受理时固定的处理器版本。
|
||||
HandlerVersion string `json:"handler_version"`
|
||||
// PayloadVersion 受理时固定的参数契约版本。
|
||||
PayloadVersion string `json:"payload_version"`
|
||||
// Payload 业务无关 JSON 输入,保留数字精度。
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
// Result 业务 JSON 结果;未取得结果时为空并省略。
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
// BusinessKey 可选业务关联键,不自动提供业务效果幂等。
|
||||
BusinessKey string `json:"business_key,omitempty"`
|
||||
// AvailableAt 最早可执行时间,UTC。
|
||||
AvailableAt time.Time `json:"available_at"`
|
||||
// StartDeadlineAt 最晚开始时间;为空时使用任务总截止时间。
|
||||
StartDeadlineAt *time.Time `json:"start_deadline_at,omitempty"`
|
||||
// DeadlineAt 任务总截止时间,UTC。
|
||||
DeadlineAt time.Time `json:"deadline_at"`
|
||||
// RecoverySafety 未知外部效果的恢复安全等级。
|
||||
RecoverySafety RecoverySafety `json:"recovery_safety"`
|
||||
// Retry 受理时冻结的尝试次数与时限策略。
|
||||
Retry RetryPolicy `json:"retry"`
|
||||
// AttemptCount 已创建的尝试次数,包含首次尝试。
|
||||
AttemptCount uint32 `json:"attempt_count"`
|
||||
// CancelRequestedAt 取消意图时间;为空表示未请求取消。
|
||||
CancelRequestedAt *time.Time `json:"cancel_requested_at,omitempty"`
|
||||
// CompletedAt 终结时间;非终态为空。
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
// CreatedAt 创建时间,UTC 微秒精度。
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// UpdatedAt 最后变更时间,不能替代并发版本。
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
type SubmitTaskResult struct {
|
||||
// Task 本次操作的提交回执快照;当前状态应另行查询。
|
||||
Task Task `json:"task"`
|
||||
// Deduplicated 是否复用了同一幂等身份的原始回执。
|
||||
Deduplicated bool `json:"deduplicated"`
|
||||
}
|
||||
type CancelTaskRequest struct {
|
||||
// Reason 管理操作原因,写入审计历史。
|
||||
Reason string `json:"reason"`
|
||||
// ExpectedRevision 调用者预期的资源版本,JSON 为十进制字符串。
|
||||
ExpectedRevision uint64 `json:"expected_revision,string"`
|
||||
}
|
||||
type CancelTaskResult struct {
|
||||
// Task 本次操作的提交回执快照;当前状态应另行查询。
|
||||
Task Task `json:"task"`
|
||||
// Deduplicated 是否复用了同一幂等身份的原始回执。
|
||||
Deduplicated bool `json:"deduplicated"`
|
||||
}
|
||||
type ListTasksRequest struct {
|
||||
// QueueID 队列标识,必须属于当前命名空间。
|
||||
QueueID string
|
||||
// State 当前任务状态;终态不会被重复请求重开。
|
||||
State TaskState
|
||||
// Limit 分页大小,默认 20,上限 100。
|
||||
Limit int
|
||||
// Cursor 上页返回的游标,必须复用相同查询条件。
|
||||
Cursor string
|
||||
}
|
||||
|
||||
// TaskSummary 用于列表,避免在分页中重复传输大参数和结果。
|
||||
type TaskSummary struct {
|
||||
// ID 资源标识,32 位小写十六进制字符串。
|
||||
ID string `json:"id"`
|
||||
// QueueID 队列标识,必须属于当前命名空间。
|
||||
QueueID string `json:"queue_id"`
|
||||
// State 当前任务状态;终态不会被重复请求重开。
|
||||
State TaskState `json:"state"`
|
||||
// Revision 并发版本,JSON 使用十进制字符串避免跨语言精度丢失。
|
||||
Revision uint64 `json:"revision,string"`
|
||||
// Handler Worker 处理器名称。
|
||||
Handler string `json:"handler"`
|
||||
// HandlerVersion 受理时固定的处理器版本。
|
||||
HandlerVersion string `json:"handler_version"`
|
||||
// BusinessKey 可选业务关联键,不自动提供业务效果幂等。
|
||||
BusinessKey string `json:"business_key,omitempty"`
|
||||
// AvailableAt 最早可执行时间,UTC。
|
||||
AvailableAt time.Time `json:"available_at"`
|
||||
// DeadlineAt 任务总截止时间,UTC。
|
||||
DeadlineAt time.Time `json:"deadline_at"`
|
||||
// AttemptCount 已创建的尝试次数,包含首次尝试。
|
||||
AttemptCount uint32 `json:"attempt_count"`
|
||||
// CreatedAt 创建时间,UTC 微秒精度。
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// UpdatedAt 最后变更时间,不能替代并发版本。
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
type TaskPage struct {
|
||||
// Items 当前页任务摘要,不包含大体积参数与结果。
|
||||
Items []TaskSummary `json:"items"`
|
||||
// NextCursor 下一页游标;为空表示当前查询已无下一页。
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
type CreateQueueRequest struct {
|
||||
// Name 队列名称,同一命名空间内唯一。
|
||||
Name string `json:"name"`
|
||||
// MaxConcurrency 最大同时执行数量,供执行阶段使用。
|
||||
MaxConcurrency uint32 `json:"max_concurrency"`
|
||||
// RatePerSecond 每秒开始数量上限,供执行阶段使用。
|
||||
RatePerSecond uint32 `json:"rate_per_second"`
|
||||
}
|
||||
type Queue struct {
|
||||
// ID 资源标识,32 位小写十六进制字符串。
|
||||
ID string `json:"id"`
|
||||
// NamespaceID 命名空间标识,由凭据确定隔离范围。
|
||||
NamespaceID string `json:"namespace_id"`
|
||||
// Name 队列名称,同一命名空间内唯一。
|
||||
Name string `json:"name"`
|
||||
// Paused 是否停止新执行;不禁止受理新任务。
|
||||
Paused bool `json:"paused"`
|
||||
// MaxConcurrency 最大同时执行数量,供执行阶段使用。
|
||||
MaxConcurrency uint32 `json:"max_concurrency"`
|
||||
// RatePerSecond 每秒开始数量上限,供执行阶段使用。
|
||||
RatePerSecond uint32 `json:"rate_per_second"`
|
||||
// Revision 并发版本,JSON 使用十进制字符串避免跨语言精度丢失。
|
||||
Revision uint64 `json:"revision,string"`
|
||||
}
|
||||
type CreateQueueResult struct {
|
||||
// Queue 本次操作返回的队列快照。
|
||||
Queue Queue `json:"queue"`
|
||||
// Deduplicated 是否复用了同一幂等身份的原始回执。
|
||||
Deduplicated bool `json:"deduplicated"`
|
||||
}
|
||||
|
||||
// TaskState 是服务端任务状态;未知状态应保留供升级兼容判断。
|
||||
type TaskState string
|
||||
|
||||
const (
|
||||
TaskWaiting TaskState = "WAITING"
|
||||
TaskLeased TaskState = "LEASED"
|
||||
TaskRunning TaskState = "RUNNING"
|
||||
TaskNeedsReview TaskState = "NEEDS_REVIEW"
|
||||
TaskSucceeded TaskState = "SUCCEEDED"
|
||||
TaskFailed TaskState = "FAILED"
|
||||
TaskCancelled TaskState = "CANCELLED"
|
||||
)
|
||||
|
||||
// RecoverySafety 声明业务处理器面对未知外部效果时是否可以安全重入。
|
||||
type RecoverySafety string
|
||||
|
||||
const (
|
||||
RecoveryReconcileOnly RecoverySafety = "RECONCILE_ONLY"
|
||||
RecoveryIdempotent RecoverySafety = "IDEMPOTENT"
|
||||
)
|
||||
|
||||
// RetryPolicy 是有限执行策略;单位为毫秒,次数包含首次尝试。
|
||||
type RetryPolicy struct {
|
||||
MaxAttempts uint32 `json:"max_attempts"` // MaxAttempts 总尝试次数上限。
|
||||
InitialDelayMS uint64 `json:"initial_delay_ms"` // InitialDelayMS 首次重试间隔。
|
||||
MaxDelayMS uint64 `json:"max_delay_ms"` // MaxDelayMS 间隔上限。
|
||||
AttemptTimeoutMS uint64 `json:"attempt_timeout_ms"` // AttemptTimeoutMS 单次尝试时限。
|
||||
}
|
||||
Reference in New Issue
Block a user