Files
orbit/adapters/outbox/mysql/transaction_integration_test.go

180 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) }