fix(publish): require submit marker before platform write
Desktop Client Build / Resolve Build Metadata (push) Successful in 20s
Frontend CI / Frontend (push) Successful in 2m57s
Desktop Client Build / Build Desktop Client (push) Successful in 25m11s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 34s
Backend CI / Backend (push) Successful in 50m56s

This commit is contained in:
2026-05-30 22:29:28 +08:00
parent dbd2a1c9f6
commit 8ea4a2ffff
20 changed files with 227 additions and 155 deletions
@@ -722,8 +722,9 @@ func (s *DesktopTaskService) Extend(ctx context.Context, client *repository.Desk
// MarkPublishSubmitStarted records a durable intent marker right before the client performs the
// irreversible platform submit POST. Lease-recovery uses it to avoid silently re-posting a
// non-idempotent article: once submit may have started, recovery routes the task to 'unknown'
// (manual reconcile) instead of auto-requeueing it. Best-effort — a stale/lost lease is a no-op
// and must never block the in-flight publish.
// (manual reconcile) instead of auto-requeueing it. The client must receive a
// positive marker write before it performs the platform submit; a stale/lost
// lease is rejected so the client can stop before the irreversible request.
func (s *DesktopTaskService) MarkPublishSubmitStarted(ctx context.Context, client *repository.DesktopClient, taskID uuid.UUID, leaseToken string) error {
if s == nil || s.pool == nil || client == nil {
return nil
@@ -732,7 +733,7 @@ func (s *DesktopTaskService) MarkPublishSubmitStarted(ctx context.Context, clien
if leaseToken == "" {
return response.ErrBadRequest(40001, "invalid_params", "lease_token is required")
}
if _, err := s.pool.Exec(ctx, `
tag, err := s.pool.Exec(ctx, `
UPDATE desktop_tasks
SET publish_submit_started_at = COALESCE(publish_submit_started_at, NOW()),
updated_at = NOW()
@@ -741,9 +742,13 @@ func (s *DesktopTaskService) MarkPublishSubmitStarted(ctx context.Context, clien
AND kind = 'publish'
AND status = 'in_progress'
AND lease_token_hash = $3
`, taskID, client.ID, HashDesktopClientToken(leaseToken)); err != nil {
`, taskID, client.ID, HashDesktopClientToken(leaseToken))
if err != nil {
return response.ErrInternal(50200, "desktop_task_submit_marker_failed", "failed to mark publish submit started")
}
if tag.RowsAffected() == 0 {
return response.ErrConflict(40983, "desktop_task_lease_invalid", "desktop task lease token is invalid or expired")
}
return nil
}
@@ -558,17 +558,17 @@ func TestMeijiequanUpstreamThrottleSerializesRequests(t *testing.T) {
redis := goredis.NewClient(&goredis.Options{Addr: server.Addr()})
defer redis.Close()
client := NewMeijiequanClient(redis, config.MeijiequanConfig{UpstreamMinInterval: 25 * time.Millisecond})
client := NewMeijiequanClient(redis, config.MeijiequanConfig{UpstreamMinInterval: 80 * time.Millisecond})
ctx := context.Background()
if err := client.waitForUpstreamTurn(ctx, 25*time.Millisecond); err != nil {
if err := client.waitForUpstreamTurn(ctx, 80*time.Millisecond); err != nil {
t.Fatalf("first wait failed: %v", err)
}
start := time.Now()
if err := client.waitForUpstreamTurn(ctx, 25*time.Millisecond); err != nil {
if err := client.waitForUpstreamTurn(ctx, 80*time.Millisecond); err != nil {
t.Fatalf("second wait failed: %v", err)
}
if elapsed := time.Since(start); elapsed < 20*time.Millisecond {
if elapsed := time.Since(start); elapsed < 50*time.Millisecond {
t.Fatalf("expected serialized upstream wait, elapsed %s", elapsed)
}
}