provider-endpoint-sync-race.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { and, eq, isNull, sql } from "drizzle-orm";
  2. import { describe, expect, test } from "vitest";
  3. import { db } from "@/drizzle/db";
  4. import { providerEndpoints } from "@/drizzle/schema";
  5. import {
  6. createProvider,
  7. deleteProvider,
  8. findProviderById,
  9. updateProvider,
  10. } from "@/repository/provider";
  11. import {
  12. ensureProviderEndpointExistsForUrl,
  13. findProviderEndpointsByVendorAndType,
  14. tryDeleteProviderVendorIfEmpty,
  15. } from "@/repository/provider-endpoints";
  16. const run = process.env.DSN ? describe : describe.skip;
  17. function createDeferred() {
  18. let resolve: () => void;
  19. const promise = new Promise<void>((res) => {
  20. resolve = res;
  21. });
  22. return {
  23. promise,
  24. resolve: resolve!,
  25. };
  26. }
  27. run("Provider endpoint sync on edit (integration race)", () => {
  28. test("concurrent next-url insert should not break provider edit transaction", async () => {
  29. const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
  30. const oldUrl = `https://race-${suffix}.example.com/v1/messages`;
  31. const nextUrl = `https://race-${suffix}.example.com/v2/messages`;
  32. const websiteUrl = `https://vendor-${suffix}.example.com`;
  33. const created = await createProvider({
  34. name: `Race Provider ${suffix}`,
  35. url: oldUrl,
  36. key: `sk-race-${suffix}`,
  37. provider_type: "claude",
  38. website_url: websiteUrl,
  39. favicon_url: null,
  40. tpm: null,
  41. rpm: null,
  42. rpd: null,
  43. cc: null,
  44. });
  45. const vendorId = created.providerVendorId;
  46. expect(vendorId).not.toBeNull();
  47. const [previousEndpoint] = await db
  48. .select({
  49. id: providerEndpoints.id,
  50. })
  51. .from(providerEndpoints)
  52. .where(
  53. and(
  54. eq(providerEndpoints.vendorId, vendorId!),
  55. eq(providerEndpoints.providerType, created.providerType),
  56. eq(providerEndpoints.url, oldUrl),
  57. isNull(providerEndpoints.deletedAt)
  58. )
  59. )
  60. .limit(1);
  61. expect(previousEndpoint).toBeDefined();
  62. const lockAcquired = createDeferred();
  63. const releaseLock = createDeferred();
  64. const lockTask = db.transaction(async (tx) => {
  65. await tx.execute(sql`
  66. SELECT id
  67. FROM provider_endpoints
  68. WHERE id = ${previousEndpoint!.id}
  69. FOR UPDATE
  70. `);
  71. lockAcquired.resolve();
  72. await releaseLock.promise;
  73. });
  74. let updatePromise: Promise<Awaited<ReturnType<typeof updateProvider>>> | null = null;
  75. try {
  76. await lockAcquired.promise;
  77. updatePromise = updateProvider(created.id, { url: nextUrl });
  78. await ensureProviderEndpointExistsForUrl({
  79. vendorId: vendorId!,
  80. providerType: created.providerType,
  81. url: nextUrl,
  82. });
  83. releaseLock.resolve();
  84. await lockTask;
  85. const updated = await updatePromise;
  86. expect(updated).not.toBeNull();
  87. expect(updated?.url).toBe(nextUrl);
  88. const [previousAfter] = await db
  89. .select({
  90. id: providerEndpoints.id,
  91. url: providerEndpoints.url,
  92. deletedAt: providerEndpoints.deletedAt,
  93. isEnabled: providerEndpoints.isEnabled,
  94. })
  95. .from(providerEndpoints)
  96. .where(eq(providerEndpoints.id, previousEndpoint!.id))
  97. .limit(1);
  98. expect(previousAfter).toBeDefined();
  99. expect(previousAfter?.url).toBe(oldUrl);
  100. expect(previousAfter?.deletedAt).not.toBeNull();
  101. expect(previousAfter?.isEnabled).toBe(false);
  102. const activeEndpoints = await findProviderEndpointsByVendorAndType(
  103. vendorId!,
  104. created.providerType
  105. );
  106. const nextActive = activeEndpoints.filter((endpoint) => endpoint.url === nextUrl);
  107. const previousActive = activeEndpoints.filter((endpoint) => endpoint.url === oldUrl);
  108. expect(nextActive).toHaveLength(1);
  109. expect(nextActive[0]?.isEnabled).toBe(true);
  110. expect(previousActive).toHaveLength(0);
  111. const providerAfter = await findProviderById(created.id);
  112. expect(providerAfter?.url).toBe(nextUrl);
  113. } finally {
  114. releaseLock.resolve();
  115. await lockTask.catch(() => {});
  116. await deleteProvider(created.id);
  117. if (vendorId) {
  118. await tryDeleteProviderVendorIfEmpty(vendorId).catch(() => {});
  119. }
  120. if (updatePromise) {
  121. await updatePromise.catch(() => {});
  122. }
  123. }
  124. });
  125. });