127 lines
4.6 KiB
Go
127 lines
4.6 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
pulse "git.yuntuo.hk/k2safe/orbit/pulse/model"
|
|
"git.yuntuo.hk/k2safe/orbit/pulse/service"
|
|
)
|
|
|
|
func TestIntegrationDispatchPriority(t *testing.T) {
|
|
dsn := os.Getenv("ORBIT_TEST_MYSQL_DSN")
|
|
if dsn == "" {
|
|
t.Skip("requires disposable MySQL")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
db, err := Open(ctx, dsn, 16, 4)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
if err = Migrate(ctx, db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
submit := func(f *executionFixture, p int16, max uint32) service.TaskView {
|
|
t.Helper()
|
|
out, err := f.tasks.Submit(ctx, f.token, string(executionID(t)), service.SubmitInput{QueueID: f.resources.QueueID, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{}`), Priority: p, Retry: pulse.RetryPolicy{MaxAttempts: max}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out.Task
|
|
}
|
|
t.Run("priority_overtakes_recent_work_but_not_future_release", func(t *testing.T) {
|
|
f := newExecutionFixture(t, ctx, db)
|
|
low := submit(f, 0, 1)
|
|
high := submit(f, 9, 1)
|
|
later := submit(f, 9, 1)
|
|
f.exec(`UPDATE pulse_tasks SET available_at=DATE_SUB(UTC_TIMESTAMP(6),INTERVAL 5 SECOND) WHERE namespace_id=? AND id=?`, f.resources.NamespaceID, low.ID)
|
|
f.exec(`UPDATE pulse_tasks SET available_at=DATE_ADD(UTC_TIMESTAMP(6),INTERVAL 1 HOUR) WHERE namespace_id=? AND id=?`, f.resources.NamespaceID, later.ID)
|
|
a, _ := f.claim()
|
|
if a.Task.ID != high.ID || a.Task.Priority != 9 {
|
|
t.Fatal("priority not honored")
|
|
}
|
|
a, _ = f.claim()
|
|
if a.Task.ID != low.ID {
|
|
t.Fatal("priority bypassed actual availability")
|
|
}
|
|
noKeyClaim(t, f)
|
|
page, err := f.tasks.ListTasks(ctx, f.token, service.ListInput{QueueID: f.resources.QueueID, Limit: 20})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, v := range page.Items {
|
|
if v.ID == high.ID {
|
|
found = v.Priority == 9
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("summary lost frozen priority")
|
|
}
|
|
})
|
|
t.Run("old_low_priority_cannot_be_overtaken_by_unbounded_new_high_work", func(t *testing.T) {
|
|
f := newExecutionFixture(t, ctx, db)
|
|
old := submit(f, 0, 1)
|
|
f.exec(`UPDATE pulse_tasks SET available_at=DATE_SUB(UTC_TIMESTAMP(6),INTERVAL 271 SECOND) WHERE namespace_id=? AND id=?`, f.resources.NamespaceID, old.ID)
|
|
for range 24 {
|
|
submit(f, 9, 1)
|
|
}
|
|
a, _ := f.claim()
|
|
if a.Task.ID != old.ID {
|
|
t.Fatal("aged task starved behind fresh high priority prefix")
|
|
}
|
|
})
|
|
t.Run("retry_keeps_priority_and_recomputes_sort_time", func(t *testing.T) {
|
|
f := newExecutionFixture(t, ctx, db)
|
|
task := submit(f, 9, 2)
|
|
first := retryStart(t, f)
|
|
retryFinish(t, f, first, "FAILED", true)
|
|
var gap int
|
|
var priority int16
|
|
if err := db.QueryRowContext(ctx, `SELECT TIMESTAMPDIFF(SECOND,dispatch_at,available_at),priority FROM pulse_tasks WHERE namespace_id=? AND id=?`, f.resources.NamespaceID, task.ID).Scan(&gap, &priority); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gap != 270 || priority != 9 {
|
|
t.Fatal("retry lost priority or sort derivation")
|
|
}
|
|
noKeyClaim(t, f)
|
|
f.exec(`UPDATE pulse_tasks SET available_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND id=?`, f.resources.NamespaceID, task.ID)
|
|
second := retryStart(t, f)
|
|
retryFinish(t, f, second, "SUCCEEDED", false)
|
|
if f.get(task.ID).Priority != 9 {
|
|
t.Fatal("completed task lost priority")
|
|
}
|
|
})
|
|
t.Run("identity_validation_and_database_constraint", func(t *testing.T) {
|
|
f := newExecutionFixture(t, ctx, db)
|
|
in := service.SubmitInput{QueueID: f.resources.QueueID, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{}`), Priority: 0}
|
|
first, err := f.tasks.Submit(ctx, f.token, "same", in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dupe, err := f.tasks.Submit(ctx, f.token, "same", in)
|
|
if err != nil || !dupe.Deduplicated || dupe.Task.ID != first.Task.ID {
|
|
t.Fatal("default priority identity changed")
|
|
}
|
|
in.Priority = 9
|
|
_, err = f.tasks.Submit(ctx, f.token, "same", in)
|
|
expectWorkerCode(t, err, "IDEMPOTENCY_CONFLICT")
|
|
for _, p := range []int16{-1, 10} {
|
|
in.Priority = p
|
|
_, err = f.tasks.Submit(ctx, f.token, string(executionID(t)), in)
|
|
expectWorkerCode(t, err, "INVALID_ARGUMENT")
|
|
}
|
|
if _, err := db.ExecContext(ctx, `UPDATE pulse_tasks SET priority=10 WHERE namespace_id=? AND id=?`, f.resources.NamespaceID, first.Task.ID); err == nil {
|
|
t.Fatal("database priority range unchecked")
|
|
}
|
|
if _, err := db.ExecContext(ctx, `UPDATE pulse_tasks SET dispatch_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND id=?`, f.resources.NamespaceID, first.Task.ID); err == nil {
|
|
t.Fatal("generated dispatch order independently writable")
|
|
}
|
|
})
|
|
}
|