retry.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import { describe, expect, test } from "bun:test"
  2. import { SessionRetry } from "../../src/session/retry"
  3. import { MessageV2 } from "../../src/session/message-v2"
  4. function apiError(headers?: Record<string, string>): MessageV2.APIError {
  5. return new MessageV2.APIError({
  6. message: "boom",
  7. isRetryable: true,
  8. responseHeaders: headers,
  9. }).toObject() as MessageV2.APIError
  10. }
  11. describe("session.retry.getRetryDelayInMs", () => {
  12. test("doubles delay on each attempt when headers missing", () => {
  13. const error = apiError()
  14. const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.getRetryDelayInMs(error, index + 1))
  15. expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 32000, 64000, 128000, 256000, 512000, undefined])
  16. })
  17. test("prefers retry-after-ms when shorter than exponential", () => {
  18. const error = apiError({ "retry-after-ms": "1500" })
  19. expect(SessionRetry.getRetryDelayInMs(error, 4)).toBe(1500)
  20. })
  21. test("uses retry-after seconds when reasonable", () => {
  22. const error = apiError({ "retry-after": "30" })
  23. expect(SessionRetry.getRetryDelayInMs(error, 3)).toBe(30000)
  24. })
  25. test("accepts http-date retry-after values", () => {
  26. const date = new Date(Date.now() + 20000).toUTCString()
  27. const error = apiError({ "retry-after": date })
  28. const delay = SessionRetry.getRetryDelayInMs(error, 1)
  29. expect(delay).toBeGreaterThanOrEqual(19000)
  30. expect(delay).toBeLessThanOrEqual(20000)
  31. })
  32. test("ignores invalid retry hints", () => {
  33. const error = apiError({ "retry-after": "not-a-number" })
  34. expect(SessionRetry.getRetryDelayInMs(error, 1)).toBe(2000)
  35. })
  36. test("ignores malformed date retry hints", () => {
  37. const error = apiError({ "retry-after": "Invalid Date String" })
  38. expect(SessionRetry.getRetryDelayInMs(error, 1)).toBe(2000)
  39. })
  40. test("ignores past date retry hints", () => {
  41. const pastDate = new Date(Date.now() - 5000).toUTCString()
  42. const error = apiError({ "retry-after": pastDate })
  43. expect(SessionRetry.getRetryDelayInMs(error, 1)).toBe(2000)
  44. })
  45. test("returns undefined when delay exceeds 10 minutes", () => {
  46. const error = apiError()
  47. expect(SessionRetry.getRetryDelayInMs(error, 10)).toBeUndefined()
  48. })
  49. test("returns undefined when retry-after exceeds 10 minutes", () => {
  50. const error = apiError({ "retry-after": "50" })
  51. expect(SessionRetry.getRetryDelayInMs(error, 1)).toBe(50000)
  52. const longError = apiError({ "retry-after-ms": "700000" })
  53. expect(SessionRetry.getRetryDelayInMs(longError, 1)).toBeUndefined()
  54. })
  55. })
  56. describe("session.retry.getBoundedDelay", () => {
  57. test("returns full delay when under time budget", () => {
  58. const error = apiError()
  59. const startTime = Date.now()
  60. const delay = SessionRetry.getBoundedDelay({
  61. error,
  62. attempt: 1,
  63. startTime,
  64. })
  65. expect(delay).toBe(2000)
  66. })
  67. test("returns remaining time when delay exceeds budget", () => {
  68. const error = apiError()
  69. const startTime = Date.now() - 598_000 // 598 seconds elapsed, 2 seconds remaining
  70. const delay = SessionRetry.getBoundedDelay({
  71. error,
  72. attempt: 1,
  73. startTime,
  74. })
  75. expect(delay).toBeGreaterThanOrEqual(1900)
  76. expect(delay).toBeLessThanOrEqual(2100)
  77. })
  78. test("returns undefined when time budget exhausted", () => {
  79. const error = apiError()
  80. const startTime = Date.now() - 600_000 // exactly 10 minutes elapsed
  81. const delay = SessionRetry.getBoundedDelay({
  82. error,
  83. attempt: 1,
  84. startTime,
  85. })
  86. expect(delay).toBeUndefined()
  87. })
  88. test("returns undefined when time budget exceeded", () => {
  89. const error = apiError()
  90. const startTime = Date.now() - 700_000 // 11+ minutes elapsed
  91. const delay = SessionRetry.getBoundedDelay({
  92. error,
  93. attempt: 1,
  94. startTime,
  95. })
  96. expect(delay).toBeUndefined()
  97. })
  98. test("respects custom maxDuration", () => {
  99. const error = apiError()
  100. const startTime = Date.now() - 58_000 // 58 seconds elapsed
  101. const delay = SessionRetry.getBoundedDelay({
  102. error,
  103. attempt: 1,
  104. startTime,
  105. maxDuration: 60_000, // 1 minute max
  106. })
  107. expect(delay).toBeGreaterThanOrEqual(1900)
  108. expect(delay).toBeLessThanOrEqual(2100)
  109. })
  110. test("caps exponential backoff to remaining time", () => {
  111. const error = apiError()
  112. const startTime = Date.now() - 595_000 // 595 seconds elapsed, 5 seconds remaining
  113. const delay = SessionRetry.getBoundedDelay({
  114. error,
  115. attempt: 5, // would normally be 32 seconds
  116. startTime,
  117. })
  118. expect(delay).toBeGreaterThanOrEqual(4900)
  119. expect(delay).toBeLessThanOrEqual(5100)
  120. })
  121. test("respects server retry-after within budget", () => {
  122. const error = apiError({ "retry-after": "30" })
  123. const startTime = Date.now() - 550_000 // 550 seconds elapsed, 50 seconds remaining
  124. const delay = SessionRetry.getBoundedDelay({
  125. error,
  126. attempt: 1,
  127. startTime,
  128. })
  129. expect(delay).toBe(30000)
  130. })
  131. test("caps server retry-after to remaining time", () => {
  132. const error = apiError({ "retry-after": "30" })
  133. const startTime = Date.now() - 590_000 // 590 seconds elapsed, 10 seconds remaining
  134. const delay = SessionRetry.getBoundedDelay({
  135. error,
  136. attempt: 1,
  137. startTime,
  138. })
  139. expect(delay).toBeGreaterThanOrEqual(9900)
  140. expect(delay).toBeLessThanOrEqual(10100)
  141. })
  142. test("returns undefined when getRetryDelayInMs returns undefined", () => {
  143. const error = apiError()
  144. const startTime = Date.now()
  145. const delay = SessionRetry.getBoundedDelay({
  146. error,
  147. attempt: 10, // exceeds RETRY_MAX_DELAY
  148. startTime,
  149. })
  150. expect(delay).toBeUndefined()
  151. })
  152. })