scheduler_shuffle_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. //go:build unit
  2. package service
  3. import (
  4. "testing"
  5. "time"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // ============ shuffleWithinSortGroups 测试 ============
  9. func TestShuffleWithinSortGroups_Empty(t *testing.T) {
  10. shuffleWithinSortGroups(nil)
  11. shuffleWithinSortGroups([]accountWithLoad{})
  12. }
  13. func TestShuffleWithinSortGroups_SingleElement(t *testing.T) {
  14. accounts := []accountWithLoad{
  15. {account: &Account{ID: 1, Priority: 1}, loadInfo: &AccountLoadInfo{LoadRate: 10}},
  16. }
  17. shuffleWithinSortGroups(accounts)
  18. require.Equal(t, int64(1), accounts[0].account.ID)
  19. }
  20. func TestShuffleWithinSortGroups_DifferentGroups_OrderPreserved(t *testing.T) {
  21. now := time.Now()
  22. earlier := now.Add(-1 * time.Hour)
  23. accounts := []accountWithLoad{
  24. {account: &Account{ID: 1, Priority: 1, LastUsedAt: &earlier}, loadInfo: &AccountLoadInfo{LoadRate: 10}},
  25. {account: &Account{ID: 2, Priority: 1, LastUsedAt: &now}, loadInfo: &AccountLoadInfo{LoadRate: 20}},
  26. {account: &Account{ID: 3, Priority: 2, LastUsedAt: &earlier}, loadInfo: &AccountLoadInfo{LoadRate: 10}},
  27. }
  28. // 每个元素都属于不同组(Priority 或 LoadRate 或 LastUsedAt 不同),顺序不变
  29. for i := 0; i < 20; i++ {
  30. cpy := make([]accountWithLoad, len(accounts))
  31. copy(cpy, accounts)
  32. shuffleWithinSortGroups(cpy)
  33. require.Equal(t, int64(1), cpy[0].account.ID)
  34. require.Equal(t, int64(2), cpy[1].account.ID)
  35. require.Equal(t, int64(3), cpy[2].account.ID)
  36. }
  37. }
  38. func TestShuffleWithinSortGroups_SameGroup_Shuffled(t *testing.T) {
  39. now := time.Now()
  40. // 同一秒的时间戳视为同一组
  41. sameSecond := time.Unix(now.Unix(), 0)
  42. sameSecond2 := time.Unix(now.Unix(), 500_000_000) // 同一秒但不同纳秒
  43. accounts := []accountWithLoad{
  44. {account: &Account{ID: 1, Priority: 1, LastUsedAt: &sameSecond}, loadInfo: &AccountLoadInfo{LoadRate: 10}},
  45. {account: &Account{ID: 2, Priority: 1, LastUsedAt: &sameSecond2}, loadInfo: &AccountLoadInfo{LoadRate: 10}},
  46. {account: &Account{ID: 3, Priority: 1, LastUsedAt: &sameSecond}, loadInfo: &AccountLoadInfo{LoadRate: 10}},
  47. }
  48. // 多次执行,验证所有 ID 都出现在第一个位置(说明确实被打乱了)
  49. seen := map[int64]bool{}
  50. for i := 0; i < 100; i++ {
  51. cpy := make([]accountWithLoad, len(accounts))
  52. copy(cpy, accounts)
  53. shuffleWithinSortGroups(cpy)
  54. seen[cpy[0].account.ID] = true
  55. // 无论怎么打乱,所有 ID 都应在候选中
  56. ids := map[int64]bool{}
  57. for _, a := range cpy {
  58. ids[a.account.ID] = true
  59. }
  60. require.True(t, ids[1] && ids[2] && ids[3])
  61. }
  62. // 至少 2 个不同的 ID 出现在首位(随机性验证)
  63. require.GreaterOrEqual(t, len(seen), 2, "shuffle should produce different orderings")
  64. }
  65. func TestShuffleWithinSortGroups_NilLastUsedAt_SameGroup(t *testing.T) {
  66. accounts := []accountWithLoad{
  67. {account: &Account{ID: 1, Priority: 1, LastUsedAt: nil}, loadInfo: &AccountLoadInfo{LoadRate: 0}},
  68. {account: &Account{ID: 2, Priority: 1, LastUsedAt: nil}, loadInfo: &AccountLoadInfo{LoadRate: 0}},
  69. {account: &Account{ID: 3, Priority: 1, LastUsedAt: nil}, loadInfo: &AccountLoadInfo{LoadRate: 0}},
  70. }
  71. seen := map[int64]bool{}
  72. for i := 0; i < 100; i++ {
  73. cpy := make([]accountWithLoad, len(accounts))
  74. copy(cpy, accounts)
  75. shuffleWithinSortGroups(cpy)
  76. seen[cpy[0].account.ID] = true
  77. }
  78. require.GreaterOrEqual(t, len(seen), 2, "nil LastUsedAt accounts should be shuffled")
  79. }
  80. func TestShuffleWithinSortGroups_MixedGroups(t *testing.T) {
  81. now := time.Now()
  82. earlier := now.Add(-1 * time.Hour)
  83. sameAsNow := time.Unix(now.Unix(), 0)
  84. // 组1: Priority=1, LoadRate=10, LastUsedAt=earlier (ID 1) — 单元素组
  85. // 组2: Priority=1, LoadRate=20, LastUsedAt=now (ID 2, 3) — 双元素组
  86. // 组3: Priority=2, LoadRate=10, LastUsedAt=earlier (ID 4) — 单元素组
  87. accounts := []accountWithLoad{
  88. {account: &Account{ID: 1, Priority: 1, LastUsedAt: &earlier}, loadInfo: &AccountLoadInfo{LoadRate: 10}},
  89. {account: &Account{ID: 2, Priority: 1, LastUsedAt: &now}, loadInfo: &AccountLoadInfo{LoadRate: 20}},
  90. {account: &Account{ID: 3, Priority: 1, LastUsedAt: &sameAsNow}, loadInfo: &AccountLoadInfo{LoadRate: 20}},
  91. {account: &Account{ID: 4, Priority: 2, LastUsedAt: &earlier}, loadInfo: &AccountLoadInfo{LoadRate: 10}},
  92. }
  93. for i := 0; i < 20; i++ {
  94. cpy := make([]accountWithLoad, len(accounts))
  95. copy(cpy, accounts)
  96. shuffleWithinSortGroups(cpy)
  97. // 组间顺序不变
  98. require.Equal(t, int64(1), cpy[0].account.ID, "group 1 position fixed")
  99. require.Equal(t, int64(4), cpy[3].account.ID, "group 3 position fixed")
  100. // 组2 内部可以打乱,但仍在位置 1 和 2
  101. mid := map[int64]bool{cpy[1].account.ID: true, cpy[2].account.ID: true}
  102. require.True(t, mid[2] && mid[3], "group 2 elements should stay in positions 1-2")
  103. }
  104. }
  105. // ============ shuffleWithinPriorityAndLastUsed 测试 ============
  106. func TestShuffleWithinPriorityAndLastUsed_Empty(t *testing.T) {
  107. shuffleWithinPriorityAndLastUsed(nil, false)
  108. shuffleWithinPriorityAndLastUsed([]*Account{}, false)
  109. }
  110. func TestShuffleWithinPriorityAndLastUsed_SingleElement(t *testing.T) {
  111. accounts := []*Account{{ID: 1, Priority: 1}}
  112. shuffleWithinPriorityAndLastUsed(accounts, false)
  113. require.Equal(t, int64(1), accounts[0].ID)
  114. }
  115. func TestShuffleWithinPriorityAndLastUsed_SameGroup_Shuffled(t *testing.T) {
  116. accounts := []*Account{
  117. {ID: 1, Priority: 1, LastUsedAt: nil},
  118. {ID: 2, Priority: 1, LastUsedAt: nil},
  119. {ID: 3, Priority: 1, LastUsedAt: nil},
  120. }
  121. seen := map[int64]bool{}
  122. for i := 0; i < 100; i++ {
  123. cpy := make([]*Account, len(accounts))
  124. copy(cpy, accounts)
  125. shuffleWithinPriorityAndLastUsed(cpy, false)
  126. seen[cpy[0].ID] = true
  127. }
  128. require.GreaterOrEqual(t, len(seen), 2, "same group should be shuffled")
  129. }
  130. func TestShuffleWithinPriorityAndLastUsed_DifferentPriority_OrderPreserved(t *testing.T) {
  131. accounts := []*Account{
  132. {ID: 1, Priority: 1, LastUsedAt: nil},
  133. {ID: 2, Priority: 2, LastUsedAt: nil},
  134. {ID: 3, Priority: 3, LastUsedAt: nil},
  135. }
  136. for i := 0; i < 20; i++ {
  137. cpy := make([]*Account, len(accounts))
  138. copy(cpy, accounts)
  139. shuffleWithinPriorityAndLastUsed(cpy, false)
  140. require.Equal(t, int64(1), cpy[0].ID)
  141. require.Equal(t, int64(2), cpy[1].ID)
  142. require.Equal(t, int64(3), cpy[2].ID)
  143. }
  144. }
  145. func TestShuffleWithinPriorityAndLastUsed_DifferentLastUsedAt_OrderPreserved(t *testing.T) {
  146. now := time.Now()
  147. earlier := now.Add(-1 * time.Hour)
  148. accounts := []*Account{
  149. {ID: 1, Priority: 1, LastUsedAt: nil},
  150. {ID: 2, Priority: 1, LastUsedAt: &earlier},
  151. {ID: 3, Priority: 1, LastUsedAt: &now},
  152. }
  153. for i := 0; i < 20; i++ {
  154. cpy := make([]*Account, len(accounts))
  155. copy(cpy, accounts)
  156. shuffleWithinPriorityAndLastUsed(cpy, false)
  157. require.Equal(t, int64(1), cpy[0].ID)
  158. require.Equal(t, int64(2), cpy[1].ID)
  159. require.Equal(t, int64(3), cpy[2].ID)
  160. }
  161. }
  162. // ============ sameLastUsedAt 测试 ============
  163. func TestSameLastUsedAt(t *testing.T) {
  164. now := time.Now()
  165. sameSecond := time.Unix(now.Unix(), 0)
  166. sameSecondDiffNano := time.Unix(now.Unix(), 999_999_999)
  167. differentSecond := now.Add(1 * time.Second)
  168. t.Run("both nil", func(t *testing.T) {
  169. require.True(t, sameLastUsedAt(nil, nil))
  170. })
  171. t.Run("one nil one not", func(t *testing.T) {
  172. require.False(t, sameLastUsedAt(nil, &now))
  173. require.False(t, sameLastUsedAt(&now, nil))
  174. })
  175. t.Run("same second different nanoseconds", func(t *testing.T) {
  176. require.True(t, sameLastUsedAt(&sameSecond, &sameSecondDiffNano))
  177. })
  178. t.Run("different seconds", func(t *testing.T) {
  179. require.False(t, sameLastUsedAt(&now, &differentSecond))
  180. })
  181. t.Run("exact same time", func(t *testing.T) {
  182. require.True(t, sameLastUsedAt(&now, &now))
  183. })
  184. }
  185. // ============ sameAccountWithLoadGroup 测试 ============
  186. func TestSameAccountWithLoadGroup(t *testing.T) {
  187. now := time.Now()
  188. sameSecond := time.Unix(now.Unix(), 0)
  189. t.Run("same group", func(t *testing.T) {
  190. a := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: &now}, loadInfo: &AccountLoadInfo{LoadRate: 10}}
  191. b := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: &sameSecond}, loadInfo: &AccountLoadInfo{LoadRate: 10}}
  192. require.True(t, sameAccountWithLoadGroup(a, b))
  193. })
  194. t.Run("different priority", func(t *testing.T) {
  195. a := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: &now}, loadInfo: &AccountLoadInfo{LoadRate: 10}}
  196. b := accountWithLoad{account: &Account{Priority: 2, LastUsedAt: &now}, loadInfo: &AccountLoadInfo{LoadRate: 10}}
  197. require.False(t, sameAccountWithLoadGroup(a, b))
  198. })
  199. t.Run("different load rate", func(t *testing.T) {
  200. a := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: &now}, loadInfo: &AccountLoadInfo{LoadRate: 10}}
  201. b := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: &now}, loadInfo: &AccountLoadInfo{LoadRate: 20}}
  202. require.False(t, sameAccountWithLoadGroup(a, b))
  203. })
  204. t.Run("different last used at", func(t *testing.T) {
  205. later := now.Add(1 * time.Second)
  206. a := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: &now}, loadInfo: &AccountLoadInfo{LoadRate: 10}}
  207. b := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: &later}, loadInfo: &AccountLoadInfo{LoadRate: 10}}
  208. require.False(t, sameAccountWithLoadGroup(a, b))
  209. })
  210. t.Run("both nil LastUsedAt", func(t *testing.T) {
  211. a := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: nil}, loadInfo: &AccountLoadInfo{LoadRate: 0}}
  212. b := accountWithLoad{account: &Account{Priority: 1, LastUsedAt: nil}, loadInfo: &AccountLoadInfo{LoadRate: 0}}
  213. require.True(t, sameAccountWithLoadGroup(a, b))
  214. })
  215. }
  216. // ============ sameAccountGroup 测试 ============
  217. func TestSameAccountGroup(t *testing.T) {
  218. now := time.Now()
  219. t.Run("same group", func(t *testing.T) {
  220. a := &Account{Priority: 1, LastUsedAt: nil}
  221. b := &Account{Priority: 1, LastUsedAt: nil}
  222. require.True(t, sameAccountGroup(a, b))
  223. })
  224. t.Run("different priority", func(t *testing.T) {
  225. a := &Account{Priority: 1, LastUsedAt: nil}
  226. b := &Account{Priority: 2, LastUsedAt: nil}
  227. require.False(t, sameAccountGroup(a, b))
  228. })
  229. t.Run("different LastUsedAt", func(t *testing.T) {
  230. later := now.Add(1 * time.Second)
  231. a := &Account{Priority: 1, LastUsedAt: &now}
  232. b := &Account{Priority: 1, LastUsedAt: &later}
  233. require.False(t, sameAccountGroup(a, b))
  234. })
  235. }
  236. // ============ sortAccountsByPriorityAndLastUsed 集成随机化测试 ============
  237. func TestSortAccountsByPriorityAndLastUsed_WithShuffle(t *testing.T) {
  238. t.Run("same priority and nil LastUsedAt are shuffled", func(t *testing.T) {
  239. accounts := []*Account{
  240. {ID: 1, Priority: 1, LastUsedAt: nil},
  241. {ID: 2, Priority: 1, LastUsedAt: nil},
  242. {ID: 3, Priority: 1, LastUsedAt: nil},
  243. }
  244. seen := map[int64]bool{}
  245. for i := 0; i < 100; i++ {
  246. cpy := make([]*Account, len(accounts))
  247. copy(cpy, accounts)
  248. sortAccountsByPriorityAndLastUsed(cpy, false)
  249. seen[cpy[0].ID] = true
  250. }
  251. require.GreaterOrEqual(t, len(seen), 2, "identical sort keys should produce different orderings after shuffle")
  252. })
  253. t.Run("different priorities still sorted correctly", func(t *testing.T) {
  254. now := time.Now()
  255. accounts := []*Account{
  256. {ID: 3, Priority: 3, LastUsedAt: &now},
  257. {ID: 1, Priority: 1, LastUsedAt: &now},
  258. {ID: 2, Priority: 2, LastUsedAt: &now},
  259. }
  260. sortAccountsByPriorityAndLastUsed(accounts, false)
  261. require.Equal(t, int64(1), accounts[0].ID)
  262. require.Equal(t, int64(2), accounts[1].ID)
  263. require.Equal(t, int64(3), accounts[2].ID)
  264. })
  265. }