Files
orbit/sdk/go/tasks.go

179 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package orbit
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
// APIError 提供可分支处理的错误码和请求追踪标识,不包含内部数据库错误。
type APIError struct {
StatusCode int
Code string
Message string
RequestID string
// RetryAfter 服务端建议的最小退避时间;缺失、无效或超过一小时返回零,SDK 不自动重试。
RetryAfter time.Duration
}
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 保存取消意图;运行中等待结果确认,重复请求保留原键与原版本。
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 = marshalWire(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) {
var retryAfter time.Duration
if seconds, err := strconv.ParseUint(response.Header.Get("Retry-After"), 10, 32); err == nil && seconds > 0 && seconds <= 3600 {
retryAfter = time.Duration(seconds) * time.Second
}
return &APIError{StatusCode: response.StatusCode, Code: envelope.Error.Code, Message: envelope.Error.Message, RequestID: envelope.RequestID, RetryAfter: retryAfter}
}
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)
}
// ConfigureQueue 完整替换队列控制参数;重复请求保留原版本及幂等键。
func (c *Client) ConfigureQueue(ctx context.Context, id, key string, in ConfigureQueueRequest) (CreateQueueResult, error) {
var out CreateQueueResult
if !validID(id) {
return out, errors.New("invalid queue id")
}
err := c.call(ctx, "POST", "/v1/queues/"+id+":configure", key, in, &out)
return out, err
}