| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- package product
- import (
- "context"
- "errors"
- "fmt"
- "sync"
- "sync/atomic"
- "testing"
- "time"
- "perms-system-server/internal/testutil"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计 M-6 的新增 LockByCodeTx —— "在当前事务里 SELECT ... FOR UPDATE 锁住 product 行"。
- // 本方法是后续将 SyncPermissions 串行化到每个 product 的基础设施。契约:
- // 1) 存在的 code 必须正常返回全字段;
- // 2) 不存在的 code 必须返回 sqlx.ErrNotFound 以便调用方 fail-close;
- // 3) 行锁必须真实 —— 两个事务对同一 code 并发 FOR UPDATE,
- // 先拿到锁的那个必须让后者阻塞到前者 commit/rollback 为止。
- // ---------------------------------------------------------------------------
- // TC-0809: LockByCodeTx 对存在的 code 返回完整数据。
- func TestSysProductModel_LockByCodeTx_Found(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
- var got *SysProduct
- require.NoError(t, m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- got, err = m.LockByCodeTx(c, session, p.Code)
- return err
- }))
- require.NotNil(t, got)
- assert.Equal(t, id, got.Id)
- assert.Equal(t, p.Code, got.Code)
- assert.Equal(t, p.AppKey, got.AppKey)
- assert.Equal(t, p.Status, got.Status, "锁行时不得过滤禁用态,否则 SyncPermissions 无法为禁用产品正确 fail-close")
- }
- // TC-0810: LockByCodeTx 对不存在的 code 返回 sqlx.ErrNotFound。
- func TestSysProductModel_LockByCodeTx_NotFound(t *testing.T) {
- ctx := context.Background()
- m := newTestModel(t)
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- _, e := m.LockByCodeTx(c, session, "definitely_no_such_code_"+testutil.UniqueId())
- return e
- })
- require.Error(t, err)
- assert.True(t, errors.Is(err, sqlx.ErrNotFound),
- "LockByCodeTx 对不存在的 code 必须返回 ErrNotFound,便于上层 fail-close 返回 401/404")
- }
- // TC-0811: FOR UPDATE 行锁真实生效 —— 两个事务同时尝试锁同一行时,
- // 后进者必须被阻塞直到先进者结束事务。
- // 测量方式:goroutine A 在 tx 内 Lock 住后 sleep 500ms 再 commit;
- // goroutine B 等 100ms 后也尝试 Lock 同一行,记录耗时。B 的耗时必须≥400ms。
- func TestSysProductModel_LockByCodeTx_BlocksConcurrentWriter(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
- var (
- wg sync.WaitGroup
- aHoldMs = int64(500)
- bStartDelayMs = int64(100)
- bElapsedNanos int64
- aFinishedNanos int64
- aErr, bErr error
- )
- wg.Add(2)
- go func() {
- defer wg.Done()
- aErr = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- if _, e := m.LockByCodeTx(c, session, p.Code); e != nil {
- return e
- }
- // A 拿到锁后故意延时,模拟一段业务处理期。期间 B 必须被阻塞。
- time.Sleep(time.Duration(aHoldMs) * time.Millisecond)
- atomic.StoreInt64(&aFinishedNanos, time.Now().UnixNano())
- return nil
- })
- }()
- go func() {
- defer wg.Done()
- time.Sleep(time.Duration(bStartDelayMs) * time.Millisecond)
- start := time.Now()
- bErr = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- _, e := m.LockByCodeTx(c, session, p.Code)
- return e
- })
- atomic.StoreInt64(&bElapsedNanos, time.Since(start).Nanoseconds())
- }()
- wg.Wait()
- require.NoError(t, aErr)
- require.NoError(t, bErr)
- // B 的耗时 ≥ A 的剩余持锁时间。A 持 500ms,B 延 100ms 后入场,
- // 因此 B 被阻塞的时间至少 (500-100)=400ms。给 DB 一点抖动放到 300ms。
- elapsedMs := atomic.LoadInt64(&bElapsedNanos) / int64(time.Millisecond)
- minBlockedMs := int64(300)
- assert.GreaterOrEqualf(t, elapsedMs, minBlockedMs, fmt.Sprintf(
- "B 的 LockByCodeTx 总耗时 %dms 明显低于预期最小阻塞 %dms —— "+
- "意味着 FOR UPDATE 行锁失效,M-6 声称的'按 product 串行化'不成立",
- elapsedMs, minBlockedMs))
- }
|