ent_init_retry_test.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. package repository
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "testing"
  7. "time"
  8. "github.com/lib/pq"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func transientDatabaseError(code string) error {
  12. return &pq.Error{Code: pq.ErrorCode(code), Message: "database is temporarily unavailable"}
  13. }
  14. func TestIsTransientDatabaseInitializationError(t *testing.T) {
  15. tests := []struct {
  16. name string
  17. err error
  18. want bool
  19. }{
  20. {name: "postgres is starting", err: transientDatabaseError("57P03"), want: true},
  21. {name: "connection failure", err: fmt.Errorf("wrapped: %w", transientDatabaseError("08006")), want: true},
  22. {name: "authentication failure", err: transientDatabaseError("28P01"), want: false},
  23. {name: "migration error", err: errors.New("migration checksum mismatch"), want: false},
  24. }
  25. for _, tt := range tests {
  26. t.Run(tt.name, func(t *testing.T) {
  27. require.Equal(t, tt.want, isTransientDatabaseInitializationError(tt.err))
  28. })
  29. }
  30. }
  31. func TestInitializeDatabaseWithRetryEventuallySucceeds(t *testing.T) {
  32. attempts := 0
  33. var delays []time.Duration
  34. err := initializeDatabaseWithRetryWithWait(context.Background(), func(context.Context) error {
  35. attempts++
  36. if attempts <= 3 {
  37. return transientDatabaseError("57P03")
  38. }
  39. return nil
  40. }, func(_ context.Context, delay time.Duration) error {
  41. delays = append(delays, delay)
  42. return nil
  43. })
  44. require.NoError(t, err)
  45. require.Equal(t, 4, attempts)
  46. require.Equal(t, []time.Duration{time.Second, 2 * time.Second, 4 * time.Second}, delays)
  47. }
  48. func TestInitializeDatabaseWithRetryFailsFastForPermanentError(t *testing.T) {
  49. attempts := 0
  50. waitCalled := false
  51. permanentErr := transientDatabaseError("28P01")
  52. err := initializeDatabaseWithRetryWithWait(context.Background(), func(context.Context) error {
  53. attempts++
  54. return permanentErr
  55. }, func(_ context.Context, _ time.Duration) error {
  56. waitCalled = true
  57. return nil
  58. })
  59. require.ErrorIs(t, err, permanentErr)
  60. require.Equal(t, 1, attempts)
  61. require.False(t, waitCalled)
  62. }
  63. func TestInitializeDatabaseWithRetryStopsWhenContextIsCanceled(t *testing.T) {
  64. ctx, cancel := context.WithCancel(context.Background())
  65. defer cancel()
  66. attempts := 0
  67. err := initializeDatabaseWithRetryWithWait(ctx, func(context.Context) error {
  68. attempts++
  69. return transientDatabaseError("57P03")
  70. }, func(ctx context.Context, _ time.Duration) error {
  71. cancel()
  72. return ctx.Err()
  73. })
  74. require.ErrorIs(t, err, context.Canceled)
  75. require.Equal(t, 1, attempts)
  76. }
  77. func TestInitializeDatabaseWithRetryReturnsLastErrorAfterLimit(t *testing.T) {
  78. attempts := 0
  79. lastErr := transientDatabaseError("08006")
  80. var delays []time.Duration
  81. err := initializeDatabaseWithRetryWithWait(context.Background(), func(context.Context) error {
  82. attempts++
  83. return lastErr
  84. }, func(_ context.Context, delay time.Duration) error {
  85. delays = append(delays, delay)
  86. return nil
  87. })
  88. require.ErrorIs(t, err, lastErr)
  89. require.Equal(t, maxDatabaseInitializationRetries+1, attempts)
  90. require.Len(t, delays, maxDatabaseInitializationRetries)
  91. require.Equal(t, databaseInitializationRetryMax, delays[len(delays)-1])
  92. }
  93. func TestInitializeDatabaseWithRetryAllowsIdempotentMigrationRetry(t *testing.T) {
  94. applied := make(map[string]bool)
  95. attempts := 0
  96. err := initializeDatabaseWithRetryWithWait(context.Background(), func(context.Context) error {
  97. attempts++
  98. if !applied["001_init.sql"] {
  99. applied["001_init.sql"] = true
  100. return transientDatabaseError("57P03")
  101. }
  102. return nil
  103. }, func(_ context.Context, _ time.Duration) error { return nil })
  104. require.NoError(t, err)
  105. require.Equal(t, 2, attempts)
  106. require.True(t, applied["001_init.sql"])
  107. }