fix(api): align claim bounds across runtime snapshots
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user