group_usage_rollup_trigger_integration_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. //go:build integration
  2. package repository
  3. import (
  4. "context"
  5. "database/sql"
  6. "fmt"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/migrations"
  10. "github.com/lib/pq"
  11. "github.com/stretchr/testify/require"
  12. )
  13. func TestGroupUsageRollupTriggerInvalidatesCascadedHistoricalDelete(t *testing.T) {
  14. for _, partitioned := range []bool{false, true} {
  15. name := "ordinary"
  16. if partitioned {
  17. name = "partitioned"
  18. }
  19. t.Run(name, func(t *testing.T) {
  20. ctx := context.Background()
  21. schema := createGroupUsageRollupTriggerTestSchema(t, ctx, partitioned)
  22. tx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  23. defer func() { _ = tx.Rollback() }()
  24. _, err := tx.ExecContext(ctx, `
  25. INSERT INTO groups (id) VALUES (10);
  26. INSERT INTO users (id) VALUES (1);
  27. INSERT INTO usage_logs (id, user_id, group_id, actual_cost, created_at)
  28. VALUES (1, 1, 10, 1.25, TIMESTAMPTZ '2020-01-02 08:00:00+08');
  29. UPDATE usage_group_rollup_state
  30. SET closed_before = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date
  31. WHERE id = 1;
  32. DELETE FROM users WHERE id = 1;
  33. `)
  34. require.NoError(t, err)
  35. var closedBefore string
  36. err = tx.QueryRowContext(ctx, `
  37. SELECT closed_before::text
  38. FROM usage_group_rollup_state
  39. WHERE id = 1
  40. `).Scan(&closedBefore)
  41. require.NoError(t, err)
  42. require.Equal(t, "2020-01-02", closedBefore)
  43. })
  44. }
  45. }
  46. func TestGroupUsageRollupTriggerSerializesLateHistoricalInsertWithPublish(t *testing.T) {
  47. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  48. defer cancel()
  49. schema := createGroupUsageRollupTriggerTestSchema(t, ctx, false)
  50. seedTx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  51. _, err := seedTx.ExecContext(ctx, `
  52. INSERT INTO groups (id) VALUES (10);
  53. INSERT INTO users (id) VALUES (1);
  54. UPDATE usage_group_rollup_state
  55. SET closed_before = DATE '2020-01-02'
  56. WHERE id = 1;
  57. `)
  58. require.NoError(t, err)
  59. require.NoError(t, seedTx.Commit())
  60. syncTx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  61. defer func() { _ = syncTx.Rollback() }()
  62. var stateID int16
  63. require.NoError(t, syncTx.QueryRowContext(ctx, `
  64. SELECT id
  65. FROM usage_group_rollup_state
  66. WHERE id = 1
  67. FOR UPDATE
  68. `).Scan(&stateID))
  69. lateTx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  70. defer func() { _ = lateTx.Rollback() }()
  71. var lateBackendPID int
  72. require.NoError(t, lateTx.QueryRowContext(ctx, "SELECT pg_backend_pid()").Scan(&lateBackendPID))
  73. insertResult := make(chan error, 1)
  74. go func() {
  75. _, insertErr := lateTx.ExecContext(ctx, `
  76. INSERT INTO usage_logs (id, user_id, group_id, actual_cost, created_at)
  77. VALUES (1, 1, 10, 1.25, TIMESTAMPTZ '2020-01-02 09:00:00+08')
  78. `)
  79. insertResult <- insertErr
  80. }()
  81. blocked, err := waitForGroupUsageRollupStateLock(ctx, lateBackendPID, insertResult)
  82. if err != nil || !blocked {
  83. _ = syncTx.Rollback()
  84. _ = lateTx.Rollback()
  85. require.NoError(t, err)
  86. require.True(t, blocked, "迟到写入必须等待正在发布水位的事务")
  87. }
  88. _, err = syncTx.ExecContext(ctx, `
  89. UPDATE usage_group_rollup_state
  90. SET closed_before = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date
  91. WHERE id = 1
  92. `)
  93. require.NoError(t, err)
  94. require.NoError(t, syncTx.Commit())
  95. select {
  96. case err = <-insertResult:
  97. require.NoError(t, err)
  98. case <-ctx.Done():
  99. t.Fatal("等待迟到写入完成超时")
  100. }
  101. require.NoError(t, lateTx.Commit())
  102. var closedBefore string
  103. err = integrationDB.QueryRowContext(ctx, fmt.Sprintf(
  104. "SELECT closed_before::text FROM %s.usage_group_rollup_state WHERE id = 1",
  105. pq.QuoteIdentifier(schema),
  106. )).Scan(&closedBefore)
  107. require.NoError(t, err)
  108. require.Equal(t, "2020-01-02", closedBefore)
  109. }
  110. func TestGroupUsageRollupTriggerSerializesInsertTransactionAcrossMidnight(t *testing.T) {
  111. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  112. defer cancel()
  113. schema := createGroupUsageRollupTriggerTestSchema(t, ctx, false)
  114. seedTx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  115. _, err := seedTx.ExecContext(ctx, `
  116. INSERT INTO groups (id) VALUES (10);
  117. INSERT INTO users (id) VALUES (1);
  118. UPDATE usage_group_rollup_state
  119. SET closed_before = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date
  120. WHERE id = 1;
  121. `)
  122. require.NoError(t, err)
  123. require.NoError(t, seedTx.Commit())
  124. syncTx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  125. defer func() { _ = syncTx.Rollback() }()
  126. var stateID int16
  127. require.NoError(t, syncTx.QueryRowContext(ctx, `
  128. SELECT id
  129. FROM usage_group_rollup_state
  130. WHERE id = 1
  131. FOR UPDATE
  132. `).Scan(&stateID))
  133. insertTx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  134. defer func() { _ = insertTx.Rollback() }()
  135. require.NoError(t, setGroupUsageRollupTriggerTimeZone(ctx, insertTx, "Asia/Shanghai"))
  136. var insertBackendPID int
  137. require.NoError(t, insertTx.QueryRowContext(ctx, "SELECT pg_backend_pid()").Scan(&insertBackendPID))
  138. insertResult := make(chan error, 1)
  139. go func() {
  140. _, insertErr := insertTx.ExecContext(ctx, `
  141. INSERT INTO usage_logs (id, user_id, group_id, actual_cost, created_at)
  142. VALUES (1, 1, 10, 1.25, CURRENT_TIMESTAMP)
  143. `)
  144. insertResult <- insertErr
  145. }()
  146. blocked, err := waitForGroupUsageRollupStateLock(ctx, insertBackendPID, insertResult)
  147. if err != nil || !blocked {
  148. _ = syncTx.Rollback()
  149. _ = insertTx.Rollback()
  150. require.NoError(t, err)
  151. require.True(t, blocked, "跨越零点的在途写入必须与水位发布串行化")
  152. }
  153. _, err = syncTx.ExecContext(ctx, `
  154. UPDATE usage_group_rollup_state
  155. SET closed_before = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date + 1
  156. WHERE id = 1
  157. `)
  158. require.NoError(t, err)
  159. require.NoError(t, syncTx.Commit())
  160. select {
  161. case err = <-insertResult:
  162. require.NoError(t, err)
  163. case <-ctx.Done():
  164. t.Fatal("等待跨零点写入完成超时")
  165. }
  166. require.NoError(t, insertTx.Commit())
  167. var currentDate string
  168. require.NoError(t, integrationDB.QueryRowContext(ctx, `
  169. SELECT (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date::text
  170. `).Scan(&currentDate))
  171. var closedBefore string
  172. err = integrationDB.QueryRowContext(ctx, fmt.Sprintf(
  173. "SELECT closed_before::text FROM %s.usage_group_rollup_state WHERE id = 1",
  174. pq.QuoteIdentifier(schema),
  175. )).Scan(&closedBefore)
  176. require.NoError(t, err)
  177. require.Equal(t, currentDate, closedBefore)
  178. }
  179. func TestGroupUsageRollupTriggerKeepsWatermarkForTodayInsert(t *testing.T) {
  180. ctx := context.Background()
  181. schema := createGroupUsageRollupTriggerTestSchema(t, ctx, false)
  182. tx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  183. defer func() { _ = tx.Rollback() }()
  184. require.NoError(t, setGroupUsageRollupTriggerTimeZone(ctx, tx, "Asia/Shanghai"))
  185. _, err := tx.ExecContext(ctx, `
  186. INSERT INTO groups (id) VALUES (10);
  187. INSERT INTO users (id) VALUES (1);
  188. UPDATE usage_group_rollup_state
  189. SET closed_before = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date
  190. WHERE id = 1;
  191. INSERT INTO usage_logs (id, user_id, group_id, actual_cost, created_at)
  192. VALUES (1, 1, 10, 1.25, CURRENT_TIMESTAMP);
  193. `)
  194. require.NoError(t, err)
  195. var unchanged bool
  196. err = tx.QueryRowContext(ctx, `
  197. SELECT closed_before = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date
  198. FROM usage_group_rollup_state
  199. WHERE id = 1
  200. `).Scan(&unchanged)
  201. require.NoError(t, err)
  202. require.True(t, unchanged)
  203. }
  204. func TestGroupUsageRollupTriggerUsesSessionTimezoneAcrossDST(t *testing.T) {
  205. ctx := context.Background()
  206. schema := createGroupUsageRollupTriggerTestSchema(t, ctx, false)
  207. tx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  208. defer func() { _ = tx.Rollback() }()
  209. _, err := tx.ExecContext(ctx, `
  210. SET LOCAL TIME ZONE 'America/New_York';
  211. INSERT INTO groups (id) VALUES (10);
  212. INSERT INTO users (id) VALUES (1);
  213. UPDATE usage_group_rollup_state
  214. SET closed_before = DATE '2026-03-09',
  215. timezone_name = 'America/New_York'
  216. WHERE id = 1;
  217. INSERT INTO usage_logs (id, user_id, group_id, actual_cost, created_at)
  218. VALUES (1, 1, 10, 1.25, TIMESTAMPTZ '2026-03-08 04:30:00+00');
  219. `)
  220. require.NoError(t, err)
  221. var closedBefore string
  222. err = tx.QueryRowContext(ctx, `
  223. SELECT closed_before::text
  224. FROM usage_group_rollup_state
  225. WHERE id = 1
  226. `).Scan(&closedBefore)
  227. require.NoError(t, err)
  228. require.Equal(t, "2026-03-07", closedBefore)
  229. }
  230. func TestGroupUsageSummaryIncludesYesterdayAcrossWatermark(t *testing.T) {
  231. ctx := context.Background()
  232. useGroupUsageRepositoryTestTimezone(t, "Asia/Shanghai")
  233. todayStart := time.Date(2026, 8, 13, 16, 0, 0, 0, time.UTC)
  234. tests := []struct {
  235. name string
  236. closedBefore string
  237. includeYesterday bool
  238. }{
  239. {name: "closed_rollup", closedBefore: "2026-08-14", includeYesterday: true},
  240. {name: "raw_tail", closedBefore: "2026-08-13", includeYesterday: false},
  241. }
  242. for _, tt := range tests {
  243. t.Run(tt.name, func(t *testing.T) {
  244. schema := createGroupUsageRollupTriggerTestSchema(t, ctx, false)
  245. tx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  246. defer func() { _ = tx.Rollback() }()
  247. _, err := tx.ExecContext(ctx, `
  248. INSERT INTO groups (id) VALUES (10);
  249. INSERT INTO users (id) VALUES (1);
  250. INSERT INTO usage_logs (id, user_id, group_id, actual_cost, created_at) VALUES
  251. (1, 1, 10, 2, TIMESTAMPTZ '2026-08-12 12:00:00+08'),
  252. (2, 1, 10, 3, TIMESTAMPTZ '2026-08-13 12:00:00+08'),
  253. (3, 1, 10, 4, TIMESTAMPTZ '2026-08-14 12:00:00+08');
  254. INSERT INTO usage_group_daily_rollups (bucket_date, group_id, actual_cost, computed_at)
  255. VALUES (DATE '2026-08-12', 10, 2, NOW());
  256. `)
  257. require.NoError(t, err)
  258. if tt.includeYesterday {
  259. _, err = tx.ExecContext(ctx, `
  260. INSERT INTO usage_group_daily_rollups (bucket_date, group_id, actual_cost, computed_at)
  261. VALUES (DATE '2026-08-13', 10, 3, NOW())
  262. `)
  263. require.NoError(t, err)
  264. }
  265. _, err = tx.ExecContext(ctx, `
  266. UPDATE usage_group_rollup_state
  267. SET closed_before = $1::date,
  268. retained_from = TIMESTAMPTZ '2026-08-12 00:00:00+08'
  269. WHERE id = 1
  270. `, tt.closedBefore)
  271. require.NoError(t, err)
  272. repo := newUsageLogRepositoryWithSQL(nil, tx)
  273. result, err := repo.GetAllGroupUsageSummary(ctx, todayStart)
  274. require.NoError(t, err)
  275. require.Len(t, result, 1)
  276. require.InDelta(t, 9, result[0].TotalCost, 0.0000001)
  277. require.InDelta(t, 4, result[0].TodayCost, 0.0000001)
  278. require.InDelta(t, 3, result[0].YesterdayCost, 0.0000001)
  279. })
  280. }
  281. }
  282. func TestGroupUsageRollupSyncRebuildsAfterTimezoneChange(t *testing.T) {
  283. ctx := context.Background()
  284. useGroupUsageRepositoryTestTimezone(t, "America/New_York")
  285. todayStart := time.Date(2026, 3, 9, 4, 0, 0, 0, time.UTC)
  286. schema := createGroupUsageRollupTriggerTestSchema(t, ctx, false)
  287. tx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  288. defer func() { _ = tx.Rollback() }()
  289. _, err := tx.ExecContext(ctx, `
  290. SET LOCAL TIME ZONE 'UTC';
  291. INSERT INTO groups (id) VALUES (10);
  292. INSERT INTO users (id) VALUES (1);
  293. INSERT INTO usage_logs (id, user_id, group_id, actual_cost, created_at) VALUES
  294. (1, 1, 10, 3, TIMESTAMPTZ '2026-03-08 05:30:00+00'),
  295. (2, 1, 10, 5, TIMESTAMPTZ '2026-03-09 04:30:00+00');
  296. INSERT INTO usage_group_daily_rollups (bucket_date, group_id, actual_cost, computed_at)
  297. VALUES (DATE '2026-03-08', 10, 99, NOW());
  298. UPDATE usage_group_rollup_state
  299. SET closed_before = DATE '2026-03-09',
  300. retained_from = TIMESTAMPTZ '2026-03-08 05:30:00+00',
  301. timezone_name = 'Asia/Shanghai'
  302. WHERE id = 1;
  303. `)
  304. require.NoError(t, err)
  305. repo := newDashboardAggregationRepositoryWithSQL(tx)
  306. require.NoError(t, repo.SyncGroupUsageRollups(ctx, todayStart))
  307. var stateTimezone string
  308. var closedBefore string
  309. require.NoError(t, tx.QueryRowContext(ctx, `
  310. SELECT timezone_name, closed_before::text
  311. FROM usage_group_rollup_state
  312. WHERE id = 1
  313. `).Scan(&stateTimezone, &closedBefore))
  314. require.Equal(t, "America/New_York", stateTimezone)
  315. require.Equal(t, "2026-03-09", closedBefore)
  316. var rollupCost float64
  317. require.NoError(t, tx.QueryRowContext(ctx, `
  318. SELECT actual_cost
  319. FROM usage_group_daily_rollups
  320. WHERE bucket_date = DATE '2026-03-08' AND group_id = 10
  321. `).Scan(&rollupCost))
  322. require.InDelta(t, 3, rollupCost, 0.0000001)
  323. usageRepo := newUsageLogRepositoryWithSQL(nil, tx)
  324. result, err := usageRepo.GetAllGroupUsageSummary(ctx, todayStart)
  325. require.NoError(t, err)
  326. require.Len(t, result, 1)
  327. require.InDelta(t, 8, result[0].TotalCost, 0.0000001)
  328. require.InDelta(t, 5, result[0].TodayCost, 0.0000001)
  329. require.InDelta(t, 3, result[0].YesterdayCost, 0.0000001)
  330. }
  331. func TestGroupUsageSummaryUsesConfiguredDSTBoundaries(t *testing.T) {
  332. ctx := context.Background()
  333. useGroupUsageRepositoryTestTimezone(t, "America/New_York")
  334. todayStart := time.Date(2026, 3, 9, 4, 0, 0, 0, time.UTC)
  335. schema := createGroupUsageRollupTriggerTestSchema(t, ctx, false)
  336. tx := beginGroupUsageRollupTriggerTestTx(t, ctx, schema)
  337. defer func() { _ = tx.Rollback() }()
  338. _, err := tx.ExecContext(ctx, `
  339. SET LOCAL TIME ZONE 'UTC';
  340. INSERT INTO groups (id) VALUES (10);
  341. INSERT INTO users (id) VALUES (1);
  342. INSERT INTO usage_logs (id, user_id, group_id, actual_cost, created_at) VALUES
  343. (1, 1, 10, 100, TIMESTAMPTZ '2026-03-08 04:30:00+00'),
  344. (2, 1, 10, 3, TIMESTAMPTZ '2026-03-08 05:30:00+00'),
  345. (3, 1, 10, 4, TIMESTAMPTZ '2026-03-09 03:30:00+00'),
  346. (4, 1, 10, 5, TIMESTAMPTZ '2026-03-09 04:30:00+00');
  347. UPDATE usage_group_rollup_state
  348. SET closed_before = DATE '1970-01-01',
  349. retained_from = TIMESTAMPTZ '1970-01-01 00:00:00+00',
  350. timezone_name = 'America/New_York'
  351. WHERE id = 1;
  352. `)
  353. require.NoError(t, err)
  354. repo := newUsageLogRepositoryWithSQL(nil, tx)
  355. result, err := repo.GetAllGroupUsageSummary(ctx, todayStart)
  356. require.NoError(t, err)
  357. require.Len(t, result, 1)
  358. require.InDelta(t, 112, result[0].TotalCost, 0.0000001)
  359. require.InDelta(t, 5, result[0].TodayCost, 0.0000001)
  360. require.InDelta(t, 7, result[0].YesterdayCost, 0.0000001)
  361. }
  362. func createGroupUsageRollupTriggerTestSchema(t *testing.T, ctx context.Context, partitioned bool) string {
  363. t.Helper()
  364. schema := fmt.Sprintf("group_usage_rollup_trigger_%d", time.Now().UnixNano())
  365. quotedSchema := pq.QuoteIdentifier(schema)
  366. _, err := integrationDB.ExecContext(ctx, "CREATE SCHEMA "+quotedSchema)
  367. require.NoError(t, err)
  368. t.Cleanup(func() {
  369. _, _ = integrationDB.ExecContext(context.Background(), "DROP SCHEMA IF EXISTS "+quotedSchema+" CASCADE")
  370. })
  371. tx, err := integrationDB.BeginTx(ctx, nil)
  372. require.NoError(t, err)
  373. defer func() { _ = tx.Rollback() }()
  374. require.NoError(t, setGroupUsageRollupTriggerSearchPath(ctx, tx, quotedSchema))
  375. usageLogsDDL := `
  376. CREATE TABLE usage_logs (
  377. id BIGINT PRIMARY KEY,
  378. user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  379. group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL,
  380. actual_cost NUMERIC(20, 10) NOT NULL,
  381. created_at TIMESTAMPTZ NOT NULL
  382. );
  383. `
  384. if partitioned {
  385. usageLogsDDL = `
  386. CREATE TABLE usage_logs (
  387. id BIGINT NOT NULL,
  388. user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  389. group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL,
  390. actual_cost NUMERIC(20, 10) NOT NULL,
  391. created_at TIMESTAMPTZ NOT NULL
  392. ) PARTITION BY RANGE (created_at);
  393. CREATE TABLE usage_logs_default PARTITION OF usage_logs DEFAULT;
  394. `
  395. }
  396. _, err = tx.ExecContext(ctx, `
  397. CREATE TABLE users (id BIGINT PRIMARY KEY);
  398. CREATE TABLE groups (id BIGINT PRIMARY KEY);
  399. `+usageLogsDDL)
  400. require.NoError(t, err)
  401. for _, migrationName := range []string{
  402. "222_group_usage_daily_rollups.sql",
  403. "223_group_usage_rollup_timezone.sql",
  404. } {
  405. migrationSQL, readErr := migrations.FS.ReadFile(migrationName)
  406. require.NoError(t, readErr)
  407. for range 2 {
  408. _, err = tx.ExecContext(ctx, string(migrationSQL))
  409. require.NoError(t, err)
  410. }
  411. }
  412. require.NoError(t, tx.Commit())
  413. return schema
  414. }
  415. func beginGroupUsageRollupTriggerTestTx(t *testing.T, ctx context.Context, schema string) *sql.Tx {
  416. t.Helper()
  417. tx, err := integrationDB.BeginTx(ctx, nil)
  418. require.NoError(t, err)
  419. require.NoError(t, setGroupUsageRollupTriggerSearchPath(ctx, tx, pq.QuoteIdentifier(schema)))
  420. return tx
  421. }
  422. func setGroupUsageRollupTriggerSearchPath(ctx context.Context, tx *sql.Tx, quotedSchema string) error {
  423. _, err := tx.ExecContext(ctx, "SET LOCAL search_path TO "+quotedSchema)
  424. return err
  425. }
  426. func setGroupUsageRollupTriggerTimeZone(ctx context.Context, tx *sql.Tx, name string) error {
  427. _, err := tx.ExecContext(ctx, "SET LOCAL TIME ZONE "+pq.QuoteLiteral(name))
  428. return err
  429. }
  430. func waitForGroupUsageRollupStateLock(
  431. ctx context.Context,
  432. backendPID int,
  433. insertResult <-chan error,
  434. ) (bool, error) {
  435. ticker := time.NewTicker(10 * time.Millisecond)
  436. defer ticker.Stop()
  437. for {
  438. select {
  439. case err := <-insertResult:
  440. if err != nil {
  441. return false, err
  442. }
  443. return false, nil
  444. case <-ticker.C:
  445. var waitEventType sql.NullString
  446. err := integrationDB.QueryRowContext(ctx, `
  447. SELECT wait_event_type
  448. FROM pg_stat_activity
  449. WHERE pid = $1
  450. `, backendPID).Scan(&waitEventType)
  451. if err != nil {
  452. return false, err
  453. }
  454. if waitEventType.Valid && waitEventType.String == "Lock" {
  455. return true, nil
  456. }
  457. case <-ctx.Done():
  458. return false, ctx.Err()
  459. }
  460. }
  461. }