package outbox import ( "context" "database/sql" "encoding/json" "errors" "net/http" "net/http/httptest" "sync" "testing" "time" orbit "git.yuntuo.hk/k2safe/orbit/sdk/go" ) func TestIntegrationCallerOwnedTransaction(t *testing.T) { db, _ := integrationDB(t) s, err := New(db, testID(t), testID(t), Options{}) if err != nil { t.Fatal(err) } ctx := context.Background() id := testID(t) tx, err := db.BeginTx(ctx, nil) if err != nil { t.Fatal(err) } defer tx.Rollback() dup, err := s.EnqueueTx(ctx, tx, id, testRequest(s.queue)) if err != nil || dup { t.Fatal(dup, err) } if dup, err = s.EnqueueTx(ctx, tx, id, testRequest(s.queue)); err != nil || !dup { t.Fatal("same transaction duplicate not recognized", dup, err) } // 从另一连接看不到未提交的待发任务;包不得偷偷提交,也不得发起HTTP。 if _, err = s.Status(ctx, id); !errors.Is(err, ErrNotFound) { t.Fatal("adapter committed caller transaction", err) } if _, err = s.claim(ctx); !errors.Is(err, ErrIdle) { t.Fatal("uncommitted task was claimable", err) } if err = tx.Rollback(); err != nil { t.Fatal(err) } if _, err = s.Status(ctx, id); !errors.Is(err, ErrNotFound) { t.Fatal("caller rollback lost", err) } // 先在RR事务建立旧快照,再由另一事务提交相同身份,重复核对必须是当前读。 tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) if err != nil { t.Fatal(err) } defer tx.Rollback() var n int if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM sender_outbox WHERE namespace_id=?`, s.namespace).Scan(&n); err != nil { t.Fatal(err) } other, err := db.BeginTx(ctx, nil) if err != nil { t.Fatal(err) } defer other.Rollback() if _, err = s.EnqueueTx(ctx, other, id, testRequest(s.queue)); err != nil { t.Fatal(err) } if err = other.Commit(); err != nil { t.Fatal(err) } if dup, err = s.EnqueueTx(ctx, tx, id, testRequest(s.queue)); err != nil || !dup { t.Fatal("RR current read failed", dup, err) } if err = tx.Commit(); err != nil { t.Fatal(err) } claim, err := s.claim(ctx) if err != nil { t.Fatal(err) } receipt := fakeReceipt(t, s) if err = s.finish(ctx, claim, "ACCEPTED", "ACCEPTED", 0, &receipt); err != nil { t.Fatal("optional hook requires business schema", err) } if status(t, s, id).State != "ACCEPTED" { t.Fatal("missing acceptance") } } func TestIntegrationSenderRuntimeDrain(t *testing.T) { for _, timeout := range []bool{false, true} { t.Run(map[bool]string{false: "finish_inflight", true: "bounded_timeout"}[timeout], func(t *testing.T) { db, _ := integrationDB(t) s, err := New(db, testID(t), testID(t), Options{}) if err != nil { t.Fatal(err) } id := testID(t) tx, err := db.Begin() if err != nil { t.Fatal(err) } defer tx.Rollback() if _, err = s.EnqueueTx(context.Background(), tx, id, testRequest(s.queue)); err != nil { t.Fatal(err) } if err = tx.Commit(); err != nil { t.Fatal(err) } entered := make(chan struct{}) release := make(chan struct{}) unblock := sync.OnceFunc(func() { close(release) }) receipt := fakeReceipt(t, s) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method == "GET" { writeTestJSON(w, map[string]any{"request_id": id, "data": orbit.Queue{ID: s.queue, NamespaceID: s.namespace}}) return } close(entered) select { case <-release: case <-r.Context().Done(): return } w.WriteHeader(201) writeTestJSON(w, map[string]any{"request_id": id, "data": receipt}) })) defer server.Close() defer unblock() api, err := orbit.NewClient(server.URL, orbit.Options{Token: "test"}) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() grace := time.Second if timeout { grace = 20 * time.Millisecond } done := make(chan error, 1) go func() { done <- s.Run(ctx, api, RunOptions{ShutdownTimeout: grace}) }() select { case <-entered: case <-time.After(5 * time.Second): t.Fatal("sender did not enter HTTP") } cancel() if !timeout { select { case err := <-done: t.Fatal("did not drain", err) case <-time.After(25 * time.Millisecond): } unblock() } select { case err = <-done: case <-time.After(5 * time.Second): t.Fatal("shutdown hung") } if timeout { if !errors.Is(err, ErrShutdownTimeout) { t.Fatal(err) } if status(t, s, id).State == "ACCEPTED" { t.Fatal("timeout fabricated acceptance") } } else { if err != nil || status(t, s, id).State != "ACCEPTED" { t.Fatal(err) } } }) } } func writeTestJSON(w http.ResponseWriter, v any) { _ = json.NewEncoder(w).Encode(v) }