fix(api): align claim bounds across runtime snapshots

This commit is contained in:
QiuSW
2026-08-05 01:39:35 +08:00
parent 7ea1c5349f
commit f6cd65208d
10 changed files with 165 additions and 53 deletions
+20 -8
View File
@@ -647,6 +647,9 @@ func (store *Store) responseFor(record claimRecord, responseLease string) (Claim
type candidate struct {
AuthorizationID, TaskID, Title, GoodsID, SKUColor, SKUSize, TotalPriceCap string
TaskVersion, Quantity int
AuthorizationTaskVersion, AuthorizationQuantity int
AuthorizationGoodsID, AuthorizationSKUColor, AuthorizationSKUSize string
AuthorizationTotalPriceCap string
AuthorizationExpiresText string
AuthorizationExpiresAt time.Time
}
@@ -654,16 +657,12 @@ type candidate struct {
func findCandidate(ctx context.Context, transaction *sql.Tx, now time.Time) (candidate, bool, error) {
rows, err := transaction.QueryContext(ctx, `SELECT authorizations.id, tasks.id, tasks.version,
tasks.title, tasks.goods_id, tasks.sku_color, tasks.sku_size, tasks.quantity,
tasks.max_total_price, authorizations.expires_at
tasks.max_total_price, authorizations.task_version, authorizations.goods_id,
authorizations.sku_color, authorizations.sku_size, authorizations.quantity,
authorizations.total_price_cap, authorizations.expires_at
FROM order_authorizations AS authorizations
JOIN tasks ON tasks.id = authorizations.task_id
WHERE authorizations.status = 'ACTIVE' AND tasks.status = 'PENDING'
AND authorizations.task_version = tasks.version
AND authorizations.goods_id = tasks.goods_id
AND authorizations.sku_color = tasks.sku_color
AND authorizations.sku_size = tasks.sku_size
AND authorizations.quantity = tasks.quantity
AND authorizations.total_price_cap = tasks.max_total_price
ORDER BY authorizations.created_at, authorizations.rowid, authorizations.id`)
if err != nil {
return candidate{}, false, err
@@ -673,6 +672,8 @@ func findCandidate(ctx context.Context, transaction *sql.Tx, now time.Time) (can
var item candidate
if err := rows.Scan(&item.AuthorizationID, &item.TaskID, &item.TaskVersion, &item.Title,
&item.GoodsID, &item.SKUColor, &item.SKUSize, &item.Quantity, &item.TotalPriceCap,
&item.AuthorizationTaskVersion, &item.AuthorizationGoodsID, &item.AuthorizationSKUColor,
&item.AuthorizationSKUSize, &item.AuthorizationQuantity, &item.AuthorizationTotalPriceCap,
&item.AuthorizationExpiresText); err != nil {
return candidate{}, false, err
}
@@ -683,6 +684,9 @@ func findCandidate(ctx context.Context, transaction *sql.Tx, now time.Time) (can
if !validCandidate(item) {
return candidate{}, false, errors.New("stored claim candidate is invalid")
}
if !candidateSnapshotMatches(item) {
continue
}
if item.AuthorizationExpiresAt.After(now) {
if err := rows.Close(); err != nil {
return candidate{}, false, err
@@ -699,7 +703,15 @@ func findCandidate(ctx context.Context, transaction *sql.Tx, now time.Time) (can
func validCandidate(item candidate) bool {
return validUUID(item.AuthorizationID) && validUUID(item.TaskID) && item.TaskVersion > 0 && item.TaskVersion < math.MaxInt &&
taskmodel.ValidTaskWireFields(item.Title, item.GoodsID, item.SKUColor, item.SKUSize, item.TotalPriceCap) &&
item.Quantity > 0
item.Quantity > 0 && item.AuthorizationTaskVersion > 0 && item.AuthorizationTaskVersion < math.MaxInt &&
taskmodel.ValidAuthorizationFields(item.AuthorizationGoodsID, item.AuthorizationSKUColor,
item.AuthorizationSKUSize, item.AuthorizationTotalPriceCap) && item.AuthorizationQuantity > 0
}
func candidateSnapshotMatches(item candidate) bool {
return item.AuthorizationTaskVersion == item.TaskVersion && item.AuthorizationGoodsID == item.GoodsID &&
item.AuthorizationSKUColor == item.SKUColor && item.AuthorizationSKUSize == item.SKUSize &&
item.AuthorizationQuantity == item.Quantity && item.AuthorizationTotalPriceCap == item.TotalPriceCap
}
func validAttemptStatus(value sql.NullString) bool {
+34 -18
View File
@@ -195,31 +195,41 @@ func TestClaimRollsBackEveryBusinessMutationOnLateFailure(t *testing.T) {
func TestClaimRejectsOutOfBoundsCandidatesWithoutBusinessMutation(t *testing.T) {
mutations := map[string]func(*testing.T, *sql.DB){
"invalid utf8 title": func(t *testing.T, database *sql.DB) {
"task invalid utf8 title": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE tasks SET title=? WHERE id=?`, string([]byte{0xff}), testTaskA)
},
"overlong title": func(t *testing.T, database *sql.DB) {
"task overlong title": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE tasks SET title=? WHERE id=?`, strings.Repeat("😀", 121), testTaskA)
},
"overlong goods id": func(t *testing.T, database *sql.DB) {
value := strings.Repeat("1", 33)
execClaimSQL(t, database, `UPDATE tasks SET goods_id=? WHERE id=?`, value, testTaskA)
execClaimSQL(t, database, `UPDATE order_authorizations SET goods_id=? WHERE id=?`, value, testAuthA)
"task overlong goods id": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE tasks SET goods_id=? WHERE id=?`, strings.Repeat("1", 33), testTaskA)
},
"overlong color": func(t *testing.T, database *sql.DB) {
value := strings.Repeat("色", 81)
execClaimSQL(t, database, `UPDATE tasks SET sku_color=? WHERE id=?`, value, testTaskA)
execClaimSQL(t, database, `UPDATE order_authorizations SET sku_color=? WHERE id=?`, value, testAuthA)
"task invalid utf8 color": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE tasks SET sku_color=? WHERE id=?`, string([]byte{0xff}), testTaskA)
},
"overlong size": func(t *testing.T, database *sql.DB) {
value := strings.Repeat("码", 81)
execClaimSQL(t, database, `UPDATE tasks SET sku_size=? WHERE id=?`, value, testTaskA)
execClaimSQL(t, database, `UPDATE order_authorizations SET sku_size=? WHERE id=?`, value, testAuthA)
"task overlong color": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE tasks SET sku_color=? WHERE id=?`, strings.Repeat("色", 81), testTaskA)
},
"overlong money": func(t *testing.T, database *sql.DB) {
value := strings.Repeat("1", 30) + ".00"
execClaimSQL(t, database, `UPDATE tasks SET max_total_price=? WHERE id=?`, value, testTaskA)
execClaimSQL(t, database, `UPDATE order_authorizations SET total_price_cap=? WHERE id=?`, value, testAuthA)
"task overlong size": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE tasks SET sku_size=? WHERE id=?`, strings.Repeat("码", 81), testTaskA)
},
"task overlong money": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE tasks SET max_total_price=? WHERE id=?`, strings.Repeat("1", 30)+".00", testTaskA)
},
"authorization overlong goods id": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE order_authorizations SET goods_id=? WHERE id=?`, strings.Repeat("1", 33), testAuthA)
},
"authorization invalid utf8 color": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE order_authorizations SET sku_color=? WHERE id=?`, string([]byte{0xff}), testAuthA)
},
"authorization overlong color": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE order_authorizations SET sku_color=? WHERE id=?`, strings.Repeat("色", 81), testAuthA)
},
"authorization overlong size": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE order_authorizations SET sku_size=? WHERE id=?`, strings.Repeat("码", 81), testAuthA)
},
"authorization overlong money": func(t *testing.T, database *sql.DB) {
execClaimSQL(t, database, `UPDATE order_authorizations SET total_price_cap=? WHERE id=?`, strings.Repeat("1", 30)+".00", testAuthA)
},
}
for name, mutate := range mutations {
@@ -234,6 +244,12 @@ func TestClaimRejectsOutOfBoundsCandidatesWithoutBusinessMutation(t *testing.T)
t.Fatalf("ClaimNext = found %v, err %v; want closed failure", found, err)
}
assertClaimState(t, database, 0, "PENDING", "ACTIVE")
for _, table := range []string{"purchase_attempts", "task_claim_requests"} {
var count int
if err := database.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count); err != nil || count != 0 {
t.Fatalf("%s rows after invalid candidate = %d, err %v", table, count, err)
}
}
})
}
}