feat: agent 本机 SimAdmin 登录与会话维持;中继设备侧限流与 agent Retry-After
- runtime.NodeAuth:agent 自查 /api/auth/status,需要时自行 POST /api/auth/login 并持 simadmin_session Cookie 转发;错误密码进入 60s 冷却(对齐控制台 ensureAuthenticated), 节点回 401/403 即失效重探,SimAdmin 重启无需重启 agent - 节点密码进 agent 配置(nodePassword),永不经中继、永不下发到手机;登录路径仍被策略 挡在电话可达面之外(TestAuthPathsAreUnreachable) - 新拒绝分类 node_auth_failed:与 upstream_error 区分,手机可提示'该节点需重新配置' - 中继:/v1/request 每设备固定窗口限流(默认 30/分钟),未鉴权调用不消耗预算 - transport:解析 Retry-After 并入退避;main loop 对 429 直接遵从中继指示 - contracts/envelope.schema.json 补 InnerResponse.outcome 字段 - 设计文档与 README 同步:WSS 改为长轮询的实现修订、响应方向已临时×临时、P2/P3 状态
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -236,6 +237,8 @@ func (c *Client) post(ctx context.Context, path string, body any, out any, sessi
|
||||
// 204 and 408 are how the relay says "nothing yet", so the agent loops straight back.
|
||||
return ErrNoWork
|
||||
case http.StatusTooManyRequests:
|
||||
retry := parseRetryAfter(response.Header.Get("Retry-After"))
|
||||
c.retryAfter = retry
|
||||
return fmt.Errorf("%w: retry after %s", ErrRateLimited, response.Header.Get("Retry-After"))
|
||||
default:
|
||||
return fmt.Errorf("transport: %s: relay returned %d", path, response.StatusCode)
|
||||
@@ -263,6 +266,16 @@ func (c *Client) post(ctx context.Context, path string, body any, out any, sessi
|
||||
// ErrRateLimited is returned when the relay asks the agent to slow down.
|
||||
var ErrRateLimited = errors.New("transport: rate limited by the relay")
|
||||
|
||||
// parseRetryAfter reads an RFC 9110 delay-seconds value. Dates are not supported and fall back to
|
||||
// the default: a clock-skewed node should not be waiting on an arithmetic date subtraction.
|
||||
func parseRetryAfter(value string) time.Duration {
|
||||
seconds, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || seconds < 0 || seconds > 3600 {
|
||||
return 2 * time.Second
|
||||
}
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
// RetryAfter reports how long the agent should wait before its next poll, honouring the relay's
|
||||
// Retry-After when it gave one.
|
||||
func (c *Client) RetryAfter() time.Duration {
|
||||
|
||||
@@ -213,3 +213,23 @@ func TestRetryAfterHasASaneDefault(t *testing.T) {
|
||||
t.Fatalf("RetryAfter() = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The relay's Retry-After is an instruction, and the loop honours it; a value the agent cannot
|
||||
// parse falls back to the default rather than becoming an unbounded wait or an immediate retry.
|
||||
func TestRetryAfterHonoursTheRelayHeader(t *testing.T) {
|
||||
client, _ := newClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Retry-After", "7")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
}), "t")
|
||||
if _, err := client.Poll(context.Background()); !errors.Is(err, ErrRateLimited) {
|
||||
t.Fatalf("got %v, want ErrRateLimited", err)
|
||||
}
|
||||
if got := client.RetryAfter(); got != 7*time.Second {
|
||||
t.Fatalf("RetryAfter() = %v, want 7s", got)
|
||||
}
|
||||
for _, junk := range []string{"", "Wed, 21 Oct 2015 07:28:00 GMT", "-3", "999999"} {
|
||||
if got := parseRetryAfter(junk); got != 2*time.Second {
|
||||
t.Errorf("parseRetryAfter(%q) = %v, want the default", junk, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user