feat: deliver authenticated task APIs, SDK and P1 regression
This commit is contained in:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user