diff --git a/server/src/external/redis/initRedisV2.ts b/server/src/external/redis/initRedisV2.ts index 59544112f..f689bf6f2 100644 --- a/server/src/external/redis/initRedisV2.ts +++ b/server/src/external/redis/initRedisV2.ts @@ -14,9 +14,10 @@ import { } from "./initUtils/redisV2Config.js"; const redisV2Config = getRedisV2ConnectionConfig({ - cacheV2Url: process.env.CACHE_V2_UPSTASH_URL, + cacheV2Url: process.env.CACHE_V2_DRAGONFLY_URL, primaryCacheUrl: process.env.CACHE_URL, currentRegion, + instanceName: "dragonfly", }); export const hasRedisV2Config = Boolean(redisV2Config); @@ -26,6 +27,7 @@ export const redisV2: Redis = redisV2Config : redis; const alternateInstanceUrls: Partial> = { + upstash: process.env.CACHE_V2_UPSTASH_URL?.trim() || undefined, redis: process.env.CACHE_V2_REDIS_URL?.trim() || undefined, dragonfly: process.env.CACHE_V2_DRAGONFLY_URL?.trim() || undefined, }; diff --git a/server/src/external/redis/initUtils/redisV2Config.ts b/server/src/external/redis/initUtils/redisV2Config.ts index 23e293255..a60083f64 100644 --- a/server/src/external/redis/initUtils/redisV2Config.ts +++ b/server/src/external/redis/initUtils/redisV2Config.ts @@ -6,16 +6,18 @@ export const getRedisV2ConnectionConfig = ({ cacheV2Url, primaryCacheUrl, currentRegion, + instanceName, }: { cacheV2Url?: string; primaryCacheUrl?: string; currentRegion: string; + instanceName: RedisV2InstanceName; }) => cacheV2Url?.trim() && cacheV2Url.trim() !== primaryCacheUrl?.trim() ? { cacheUrl: cacheV2Url.trim(), region: `${currentRegion}:v2`, - supportsUpstashShebang: supportsUpstashShebangForRedisV2("upstash"), + supportsUpstashShebang: supportsUpstashShebangForRedisV2(instanceName), commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS, } : null; diff --git a/server/src/external/redis/resolveRedisV2.ts b/server/src/external/redis/resolveRedisV2.ts index 38f2f4098..14d91f697 100644 --- a/server/src/external/redis/resolveRedisV2.ts +++ b/server/src/external/redis/resolveRedisV2.ts @@ -22,7 +22,7 @@ export const resolveRedisV2 = (): Redis => { lastLoggedInstance = activeInstance; } - if (activeInstance === "upstash") return redisV2Primary; + if (activeInstance === "dragonfly") return redisV2Primary; const alternate = getAlternateRedisV2Instance(activeInstance); return alternate ?? redisV2Primary; diff --git a/server/src/internal/misc/redisV2Cache/redisV2CacheSchemas.ts b/server/src/internal/misc/redisV2Cache/redisV2CacheSchemas.ts index 3e84a4379..c184e5b19 100644 --- a/server/src/internal/misc/redisV2Cache/redisV2CacheSchemas.ts +++ b/server/src/internal/misc/redisV2Cache/redisV2CacheSchemas.ts @@ -4,7 +4,7 @@ export const RedisV2InstanceName = z.enum(["upstash", "redis", "dragonfly"]); export type RedisV2InstanceName = z.infer; export const RedisV2CacheConfigSchema = z.object({ - activeInstance: RedisV2InstanceName.default("upstash"), + activeInstance: RedisV2InstanceName.default("dragonfly"), }); export type RedisV2CacheConfig = z.infer; diff --git a/server/src/internal/misc/redisV2Cache/redisV2CacheStore.ts b/server/src/internal/misc/redisV2Cache/redisV2CacheStore.ts index 5cc3e6cd2..7763731dc 100644 --- a/server/src/internal/misc/redisV2Cache/redisV2CacheStore.ts +++ b/server/src/internal/misc/redisV2Cache/redisV2CacheStore.ts @@ -11,7 +11,7 @@ import { const store = createEdgeConfigStore({ s3Key: ADMIN_REDIS_V2_CACHE_CONFIG_KEY, schema: RedisV2CacheConfigSchema, - defaultValue: () => ({ activeInstance: "upstash" }), + defaultValue: () => ({ activeInstance: "dragonfly" }), pollIntervalMs: ms.seconds(10), }); diff --git a/server/tests/unit/full-subject-cache/invalidateCachedFullSubject.test.ts b/server/tests/integration/others/redis/invalidate-cached-full-subject.test.ts similarity index 64% rename from server/tests/unit/full-subject-cache/invalidateCachedFullSubject.test.ts rename to server/tests/integration/others/redis/invalidate-cached-full-subject.test.ts index d1b50e16c..cfcd1144d 100644 --- a/server/tests/unit/full-subject-cache/invalidateCachedFullSubject.test.ts +++ b/server/tests/integration/others/redis/invalidate-cached-full-subject.test.ts @@ -11,12 +11,13 @@ import { buildFullSubjectKey, buildFullSubjectViewEpochKey, getCachedFullSubject, - getOrSetCachedFullSubject, invalidateCachedFullSubject, } from "@/internal/customers/cache/fullSubject/index.js"; -import { cleanupFullSubjectScenario } from "../../integration/db/full-subject/utils/cleanupFullSubjectScenario.js"; -import { buildEntitySubjectScenario } from "../../integration/db/full-subject/utils/fullSubjectScenarioBuilders.js"; -import { insertFullSubjectScenario } from "../../integration/db/full-subject/utils/insertFullSubjectScenario.js"; +import { normalizedToCachedFullSubject } from "@/internal/customers/cache/fullSubject/fullSubjectCacheModel.js"; +import { getFullSubjectNormalized } from "@/internal/customers/repos/getFullSubject/index.js"; +import { cleanupFullSubjectScenario } from "../../db/full-subject/utils/cleanupFullSubjectScenario.js"; +import { buildEntitySubjectScenario } from "../../db/full-subject/utils/fullSubjectScenarioBuilders.js"; +import { insertFullSubjectScenario } from "../../db/full-subject/utils/insertFullSubjectScenario.js"; const describeDb = process.env.TESTS_ORG ? describe : describe.skip; @@ -24,6 +25,47 @@ describeDb("invalidateCachedFullSubject", () => { let ctx: TestContext; let scenario: ReturnType; + const cleanupScenarioState = async () => { + const customerKeys = await ctx.redisV2.keys(`{${scenario.ids.customerId}}:*`); + if (customerKeys.length > 0) await ctx.redisV2.unlink(...customerKeys); + + await cleanupFullSubjectScenario({ ctx, scenario }); + }; + + const getSubjectViewEpoch = async () => { + const epoch = await ctx.redisV2.get( + buildFullSubjectViewEpochKey({ + orgId: ctx.org.id, + env: ctx.env, + customerId: scenario.ids.customerId, + }), + ); + + return epoch ? Number.parseInt(epoch, 10) : 0; + }; + + const seedCachedFullSubject = async ({ entityId }: { entityId?: string } = {}) => { + const result = await getFullSubjectNormalized({ + ctx, + customerId: scenario.ids.customerId, + entityId, + }); + if (!result) throw new Error("Failed to build full subject cache fixture"); + + const cached = normalizedToCachedFullSubject({ + normalized: result.normalized, + subjectViewEpoch: await getSubjectViewEpoch(), + }); + const subjectKey = buildFullSubjectKey({ + orgId: ctx.org.id, + env: ctx.env, + customerId: scenario.ids.customerId, + entityId, + }); + + await ctx.redisV2.set(subjectKey, JSON.stringify(cached)); + }; + beforeAll(async () => { const { createTestContext } = await import( "@tests/utils/testInitUtils/createTestContext.js" @@ -36,36 +78,19 @@ describeDb("invalidateCachedFullSubject", () => { }); beforeEach(async () => { + await cleanupScenarioState(); await insertFullSubjectScenario({ ctx, scenario }); - await getOrSetCachedFullSubject({ - ctx, - customerId: scenario.ids.customerId, - source: "invalidateCachedFullSubjectTest", - }); - await getOrSetCachedFullSubject({ - ctx, - customerId: scenario.ids.customerId, - entityId: scenario.ids.entityIds[0], - source: "invalidateCachedFullSubjectTest", - }); - await getOrSetCachedFullSubject({ - ctx, - customerId: scenario.ids.customerId, - entityId: scenario.ids.entityIds[1], - source: "invalidateCachedFullSubjectTest", - }); + await seedCachedFullSubject(); + await seedCachedFullSubject({ entityId: scenario.ids.entityIds[0] }); + await seedCachedFullSubject({ entityId: scenario.ids.entityIds[1] }); }); afterEach(async () => { - const customerKeys = await ctx.redisV2.keys(`{${scenario.ids.customerId}}:*`); - if (customerKeys.length > 0) { - await ctx.redisV2.unlink(...customerKeys); - } - - await cleanupFullSubjectScenario({ ctx, scenario }); + await cleanupScenarioState(); }); test("invalidates direct entity cache and increments subject view epoch", async () => { + const initialSubjectViewEpoch = await getSubjectViewEpoch(); const entityAKey = buildFullSubjectKey({ orgId: ctx.org.id, env: ctx.env, @@ -93,18 +118,11 @@ describeDb("invalidateCachedFullSubject", () => { expect(await ctx.redisV2.exists(customerKey)).toBe(0); expect(await ctx.redisV2.exists(entityAKey)).toBe(0); expect(await ctx.redisV2.exists(entityBKey)).toBe(1); - expect( - await ctx.redisV2.get( - buildFullSubjectViewEpochKey({ - orgId: ctx.org.id, - env: ctx.env, - customerId: scenario.ids.customerId, - }), - ), - ).toBe("1"); + expect(await getSubjectViewEpoch()).toBe(initialSubjectViewEpoch + 1); }); test("increments subject view epoch for customer invalidation", async () => { + const initialSubjectViewEpoch = await getSubjectViewEpoch(); const entityAKey = buildFullSubjectKey({ orgId: ctx.org.id, env: ctx.env, @@ -131,7 +149,9 @@ describeDb("invalidateCachedFullSubject", () => { expect(await ctx.redisV2.exists(entityAKey)).toBe(1); expect(await ctx.redisV2.exists(entityBKey)).toBe(1); - expect(await ctx.redisV2.get(epochKey)).toBe("1"); + expect(Number.parseInt((await ctx.redisV2.get(epochKey)) ?? "0", 10)).toBe( + initialSubjectViewEpoch + 1, + ); }); test("sibling entity cache becomes stale after direct entity invalidation", async () => { diff --git a/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts b/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts index 57cbfba8f..eb582e773 100644 --- a/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts +++ b/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts @@ -17,7 +17,8 @@ import chalk from "chalk"; import { setupUpdateSubscriptionIntent } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionIntent"; const customerProduct = { - prices: [], + customer_prices: [], + customer_entitlements: [], } as unknown as FullCusProduct; const baseParams: UpdateSubscriptionV1Params = { diff --git a/server/tests/unit/redis/redis-v2-config.spec.ts b/server/tests/unit/redis/redis-v2-config.spec.ts index 9e599f3a4..eea7a2031 100644 --- a/server/tests/unit/redis/redis-v2-config.spec.ts +++ b/server/tests/unit/redis/redis-v2-config.spec.ts @@ -6,27 +6,42 @@ import { } from "@/external/redis/initUtils/redisV2Config.js"; describe("redis V2 connection config", () => { - test("uses a distinct CACHE_V2_UPSTASH_URL with the Upstash shebang", () => { + test("uses a distinct CACHE_V2_DRAGONFLY_URL without the Upstash shebang", () => { expect( getRedisV2ConnectionConfig({ cacheV2Url: " redis://v2 ", primaryCacheUrl: "redis://primary", currentRegion: "us-west-2", + instanceName: "dragonfly", }), ).toEqual({ cacheUrl: "redis://v2", region: "us-west-2:v2", - supportsUpstashShebang: true, + supportsUpstashShebang: false, commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS, }); }); - test("falls back to primary Redis when CACHE_V2_UPSTASH_URL is absent or matches primary", () => { + test("uses the Upstash shebang when the upstash instance is selected", () => { + expect( + getRedisV2ConnectionConfig({ + cacheV2Url: " redis://v2 ", + primaryCacheUrl: "redis://primary", + currentRegion: "us-west-2", + instanceName: "upstash", + }), + ).toMatchObject({ + supportsUpstashShebang: true, + }); + }); + + test("falls back to primary Redis when the V2 URL is absent or matches primary", () => { expect( getRedisV2ConnectionConfig({ cacheV2Url: undefined, primaryCacheUrl: "redis://primary", currentRegion: "us-west-2", + instanceName: "dragonfly", }), ).toBeNull(); expect( @@ -34,6 +49,7 @@ describe("redis V2 connection config", () => { cacheV2Url: " redis://primary ", primaryCacheUrl: "redis://primary", currentRegion: "us-west-2", + instanceName: "dragonfly", }), ).toBeNull(); });