180 lines
4.8 KiB
Go
180 lines
4.8 KiB
Go
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) }
|