Skip to content

Commit 2e6efad

Browse files
authored
feat/lakebase-cache (#13)
feat(cache): add cache manager with Lakebase persistent storage - Add Lakebase connector for persistent cache storage - Implement cache manager with pluggable storage providers - Support in-memory and persistent storage backends - Use storage provider injection instead of boolean config - Add automatic BEGIN/COMMIT/ROLLBACK to transactions - Add probabilistic eviction to reduce query load - Use token TTL from API response - Add OpenTelemetry instrumentation for cache and Lakebase - Add cache storage tests
1 parent eab85a5 commit 2e6efad

File tree

31 files changed

+3803
-196
lines changed

31 files changed

+3803
-196
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"version": "1",
3+
"queries": {
4+
"apps_list": {
5+
"hash": "e2c65853cf4b332d638bdd30a3aefb69",
6+
"type": "{\n name: \"apps_list\";\n parameters: Record<string, never>;\n result: Array<{\n /** @sqlType STRING */\n id: string;\n /** @sqlType STRING */\n name: string;\n /** @sqlType STRING */\n creator: string;\n /** @sqlType STRING */\n tags: string;\n /** @sqlType DECIMAL(38,6) */\n totalSpend: number;\n /** @sqlType DATE */\n createdAt: string;\n }>;\n }"
7+
},
8+
"cost_recommendations": {
9+
"hash": "730c7d8b5e2726981088b5975157b0da",
10+
"type": "{\n name: \"cost_recommendations\";\n parameters: Record<string, never>;\n result: Array<{\n /** @sqlType INT */\n dummy: number;\n }>;\n }"
11+
},
12+
"example": {
13+
"hash": "aeb02ed3e8a6c77279099406f8709543",
14+
"type": "{\n name: \"example\";\n parameters: Record<string, never>;\n result: Array<{\n /** @sqlType BOOLEAN */\n \"(1 = 1)\": boolean;\n }>;\n }"
15+
},
16+
"spend_data": {
17+
"hash": "caa0430652fe15eff658e48e6dac2446",
18+
"type": "{\n name: \"spend_data\";\n parameters: {\n /** STRING - use sql.string() */\n groupBy: SQLStringMarker;\n /** STRING - use sql.string() */\n aggregationLevel: SQLStringMarker;\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n /** STRING - use sql.string() */\n appId: SQLStringMarker;\n /** STRING - use sql.string() */\n creator: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n group_key: string;\n /** @sqlType TIMESTAMP */\n aggregation_period: string;\n /** @sqlType DECIMAL(38,6) */\n cost_usd: number;\n }>;\n }"
19+
},
20+
"spend_summary": {
21+
"hash": "bbe188624c3f5904c3a7593cb32982d5",
22+
"type": "{\n name: \"spend_summary\";\n parameters: {\n /** STRING - use sql.string() */\n aggregationLevel: SQLStringMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n };\n result: Array<{\n /** @sqlType DECIMAL(33,0) */\n total: number;\n /** @sqlType DECIMAL(33,0) */\n average: number;\n /** @sqlType DECIMAL(33,0) */\n forecasted: number;\n }>;\n }"
23+
},
24+
"sql_helpers_test": {
25+
"hash": "1322df4ba9c107e8d23e2a04bae860c5",
26+
"type": "{\n name: \"sql_helpers_test\";\n parameters: {\n /** STRING - use sql.string() */\n stringParam: SQLStringMarker;\n /** NUMERIC - use sql.number() */\n numberParam: SQLNumberMarker;\n /** BOOLEAN - use sql.boolean() */\n booleanParam: SQLBooleanMarker;\n /** DATE - use sql.date() */\n dateParam: SQLDateMarker;\n /** TIMESTAMP - use sql.timestamp() */\n timestampParam: SQLTimestampMarker;\n /** STRING - use sql.string() */\n binaryParam: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n string_value: string;\n /** @sqlType STRING */\n number_value: string;\n /** @sqlType STRING */\n boolean_value: string;\n /** @sqlType STRING */\n date_value: string;\n /** @sqlType STRING */\n timestamp_value: string;\n /** @sqlType BINARY */\n binary_value: string;\n /** @sqlType STRING */\n binary_hex: string;\n /** @sqlType INT */\n binary_length: number;\n }>;\n }"
27+
},
28+
"top_contributors": {
29+
"hash": "2d58759cca2fe31dae06475a23080120",
30+
"type": "{\n name: \"top_contributors\";\n parameters: {\n /** STRING - use sql.string() */\n aggregationLevel: SQLStringMarker;\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n app_name: string;\n /** @sqlType DECIMAL(38,6) */\n total_cost_usd: number;\n }>;\n }"
31+
},
32+
"untagged_apps": {
33+
"hash": "5946262b49710b8ab458d1bf950ff8c9",
34+
"type": "{\n name: \"untagged_apps\";\n parameters: {\n /** STRING - use sql.string() */\n aggregationLevel: SQLStringMarker;\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n app_name: string;\n /** @sqlType STRING */\n creator: string;\n /** @sqlType DECIMAL(38,6) */\n total_cost_usd: number;\n /** @sqlType DECIMAL(38,10) */\n avg_period_cost_usd: number;\n }>;\n }"
35+
}
36+
}
37+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
},
3333
"lint-staged": {
3434
"(*.ts|*.tsx|*.js|*.jsx|*.json|*.md|*.yml|*.yaml|*.css)": [
35-
"pnpm lint:fix && pnpm format"
35+
"biome lint --write",
36+
"biome format --write"
3637
]
3738
},
3839
"devDependencies": {
@@ -48,6 +49,7 @@
4849
"jsdom": "^27.0.0",
4950
"lint-staged": "^15.5.1",
5051
"plop": "^4.0.4",
52+
"pg": "^8.16.3",
5153
"publint": "^0.3.15",
5254
"tsdown": "^0.15.7",
5355
"tsx": "^4.20.6",

packages/app-kit/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@
5050
"@opentelemetry/semantic-conventions": "^1.38.0",
5151
"dotenv": "^16.6.1",
5252
"express": "^4.22.0",
53+
"pg": "^8.16.3",
5354
"shared": "workspace:*",
5455
"vite": "npm:[email protected]",
5556
"ws": "^8.18.3",
5657
"zod-to-ts": "^2.0.0"
5758
},
5859
"devDependencies": {
5960
"@types/express": "^4.17.25",
61+
"@types/pg": "^8.15.6",
6062
"@types/ws": "^8.18.1",
6163
"@vitejs/plugin-react": "^5.1.1"
6264
},

packages/app-kit/src/analytics/analytics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class AnalyticsPlugin extends Plugin {
3535

3636
this.SQLClient = new SQLWarehouseConnector({
3737
timeout: config.timeout,
38-
telemetry: this.telemetry,
38+
telemetry: config.telemetry,
3939
});
4040
}
4141

packages/app-kit/src/analytics/tests/analytics.test.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,53 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
1010
import { AnalyticsPlugin, analytics } from "../analytics";
1111
import type { IAnalyticsConfig } from "../types";
1212

13+
// Mock CacheManager singleton with actual caching behavior
14+
const { mockCacheStore, mockCacheInstance } = vi.hoisted(() => {
15+
const store = new Map<string, unknown>();
16+
17+
const generateKey = (parts: unknown[], userKey: string): string => {
18+
const { createHash } = require("node:crypto");
19+
const allParts = [userKey, ...parts];
20+
const serialized = JSON.stringify(allParts);
21+
return createHash("sha256").update(serialized).digest("hex");
22+
};
23+
24+
const instance = {
25+
get: vi.fn(),
26+
set: vi.fn(),
27+
delete: vi.fn(),
28+
getOrExecute: vi.fn(
29+
async (key: unknown[], fn: () => Promise<unknown>, userKey: string) => {
30+
const cacheKey = generateKey(key, userKey);
31+
if (store.has(cacheKey)) {
32+
return store.get(cacheKey);
33+
}
34+
const result = await fn();
35+
store.set(cacheKey, result);
36+
return result;
37+
},
38+
),
39+
generateKey: vi.fn((parts: unknown[], userKey: string) =>
40+
generateKey(parts, userKey),
41+
),
42+
};
43+
44+
return { mockCacheStore: store, mockCacheInstance: instance };
45+
});
46+
47+
vi.mock("../../cache", () => ({
48+
CacheManager: {
49+
getInstanceSync: vi.fn(() => mockCacheInstance),
50+
},
51+
}));
52+
1353
describe("Analytics Plugin", () => {
1454
let config: IAnalyticsConfig;
1555

1656
beforeEach(() => {
1757
config = { timeout: 5000 };
1858
setupDatabricksEnv();
59+
mockCacheStore.clear();
1960
});
2061

2162
test("Analytics plugin data should have correct name", () => {
@@ -180,7 +221,7 @@ describe("Analytics Plugin", () => {
180221
},
181222
{
182223
userDatabricksClient: mockUserClient as any,
183-
userName: "user-token-123",
224+
userId: "user-token-123",
184225
},
185226
);
186227

@@ -277,7 +318,7 @@ describe("Analytics Plugin", () => {
277318
async () => {
278319
await handler(mockReq1, mockRes1);
279320
},
280-
{ userName: "user-token-1" },
321+
{ userId: "user-token-1" },
281322
);
282323

283324
const mockReq2 = createMockRequest({
@@ -290,7 +331,7 @@ describe("Analytics Plugin", () => {
290331
async () => {
291332
await handler(mockReq2, mockRes2);
292333
},
293-
{ userName: "user-token-2" },
334+
{ userId: "user-token-2" },
294335
);
295336

296337
const mockReq1Again = createMockRequest({
@@ -303,7 +344,7 @@ describe("Analytics Plugin", () => {
303344
async () => {
304345
await handler(mockReq1Again, mockRes1Again);
305346
},
306-
{ userName: "user-token-1" },
347+
{ userId: "user-token-1" },
307348
);
308349

309350
expect(executeMock).toHaveBeenCalledTimes(2);

packages/app-kit/src/analytics/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { BasePluginConfig } from "shared";
22

33
export interface IAnalyticsConfig extends BasePluginConfig {
44
timeout?: number;
5-
typePath?: string;
65
}
76

87
export interface IAnalyticsQueryRequest {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { CacheConfig } from "shared";
2+
3+
/** Default configuration for cache */
4+
export const cacheDefaults: CacheConfig = {
5+
enabled: true,
6+
ttl: 3600, // 1 hour
7+
maxSize: 1000, // 1000 entries
8+
cacheKey: [], // no cache key by default
9+
cleanupProbability: 0.01, // 1% probability of triggering cleanup on each get operation
10+
strictPersistence: false, // if false, use in-memory storage if lakebase is unavailable
11+
};

0 commit comments

Comments
 (0)