| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- package dept
- import (
- "context"
- "errors"
- "sync"
- "sync/atomic"
- "testing"
- "time"
- "perms-system-server/internal/testutil"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // TC-0759: M-5 修复实战回归 —— UpdateWithOptLock 在 10 个 goroutine 同时写同一行时,
- // 必须精确有 **1 个成功、9 个 ErrUpdateConflict**。
- // 旧实现依赖应用层 "先读后写", 高并发下两个 goroutine 都拿到 UpdateTime=t0 就都会写入成功
- // (最后一个覆盖前一个, 数据静默丢失). 新实现 WHERE updateTime=? 才更新, affected=0 即冲突。
- // 这是"真实并发"断言, 不是 mock, 踩在实际 MySQL 上, 为乐观锁修复提供最强的防退化护栏。
- func TestSysDeptModel_UpdateWithOptLock_ConcurrentSingleWinner(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
- base := time.Now().Unix()
- row := &SysDept{
- ParentId: 0,
- Name: "dept_optlock_" + testutil.UniqueId(),
- Path: "/optlock/" + testutil.UniqueId() + "/",
- Sort: 10,
- Remark: "orig",
- Status: 1,
- CreateTime: base,
- UpdateTime: base,
- }
- res, err := m.Insert(ctx, row)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- tbl := m.TableName()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id) })
- const workers = 10
- var (
- wg sync.WaitGroup
- success int32
- conflicts int32
- other int32
- start = make(chan struct{})
- )
- for i := 0; i < workers; i++ {
- wg.Add(1)
- go func(idx int) {
- defer wg.Done()
- <-start
- err := m.UpdateWithOptLock(ctx, &SysDept{
- Id: id,
- ParentId: 0,
- Name: row.Name,
- Path: row.Path,
- Sort: int64(idx),
- Remark: "w" + testutil.UniqueId(),
- DeptType: "NORMAL",
- Status: 1,
- CreateTime: base,
- UpdateTime: base + int64(idx+1),
- }, base)
- switch {
- case err == nil:
- atomic.AddInt32(&success, 1)
- case errors.Is(err, ErrUpdateConflict):
- atomic.AddInt32(&conflicts, 1)
- default:
- atomic.AddInt32(&other, 1)
- t.Errorf("unexpected error: %v", err)
- }
- }(i)
- }
- close(start)
- wg.Wait()
- assert.Equal(t, int32(1), atomic.LoadInt32(&success),
- "M-5: 10 个并发写必须且仅有 1 个成功, 实际 %d", success)
- assert.Equal(t, int32(workers-1), atomic.LoadInt32(&conflicts),
- "M-5: 其余 goroutine 必须全部得到 ErrUpdateConflict (无声覆盖即 BUG)")
- assert.Equal(t, int32(0), atomic.LoadInt32(&other),
- "不应出现除成功/冲突外的其他错误")
- after, err := m.FindOne(ctx, id)
- require.NoError(t, err)
- assert.NotEqual(t, base, after.UpdateTime,
- "成功的那一个必须把 UpdateTime 推进, DB 里不允许停留在初值")
- assert.Equal(t, "orig", row.Remark)
- assert.NotEqual(t, "orig", after.Remark, "胜出者必须把 Remark 更新为 w*")
- }
|