Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
feat(db): use in memory db when MONGODB_URL not set (#1773)
Browse files* feat(db): allow use of in memory db when MONGODB_URL not set
* fix(db): enhance database disconnection handling with server stop
* feat(db): persist db to disk
* fix: use proper root folder in both build & dev
- .dockerignore +2 -1
- .gitignore +2 -1
- src/lib/jobs/refresh-assistants-counts.ts +38 -38
- src/lib/migrations/migrations.ts +21 -25
- src/lib/server/database.ts +66 -13
- src/routes/assistants/+page.server.ts +4 -7
- src/routes/tools/+page.server.ts +4 -7
.dockerignore
CHANGED
@@ -8,4 +8,5 @@ node_modules/
|
|
8 |
.svelte-kit/
|
9 |
.env*
|
10 |
!.env
|
11 |
-
.env.local
|
|
|
|
8 |
.svelte-kit/
|
9 |
.env*
|
10 |
!.env
|
11 |
+
.env.local
|
12 |
+
db
|
.gitignore
CHANGED
@@ -11,4 +11,5 @@ SECRET_CONFIG
|
|
11 |
.idea
|
12 |
!.env.ci
|
13 |
!.env
|
14 |
-
gcp-*.json
|
|
|
|
11 |
.idea
|
12 |
!.env.ci
|
13 |
!.env
|
14 |
+
gcp-*.json
|
15 |
+
db
|
src/lib/jobs/refresh-assistants-counts.ts
CHANGED
@@ -26,49 +26,49 @@ async function refreshAssistantsCountsHelper() {
|
|
26 |
}
|
27 |
|
28 |
try {
|
29 |
-
await Database.getInstance()
|
30 |
-
.
|
31 |
-
|
32 |
-
session.withTransaction(async () => {
|
33 |
await Database.getInstance()
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
},
|
45 |
-
|
46 |
-
|
47 |
-
_id: "$assistantId",
|
48 |
-
last24HoursCount: { $sum: "$count" },
|
49 |
-
},
|
50 |
-
},
|
51 |
-
],
|
52 |
-
},
|
53 |
},
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
},
|
59 |
},
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
},
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
)
|
|
|
72 |
} catch (e) {
|
73 |
logger.error(e, "Refresh assistants counter failed!");
|
74 |
}
|
|
|
26 |
}
|
27 |
|
28 |
try {
|
29 |
+
await (await Database.getInstance()).getClient().withSession((session) =>
|
30 |
+
session.withTransaction(async () => {
|
31 |
+
await (
|
|
|
32 |
await Database.getInstance()
|
33 |
+
)
|
34 |
+
.getCollections()
|
35 |
+
.assistants.aggregate([
|
36 |
+
{ $project: { _id: 1 } },
|
37 |
+
{ $set: { last24HoursCount: 0 } },
|
38 |
+
{
|
39 |
+
$unionWith: {
|
40 |
+
coll: "assistants.stats",
|
41 |
+
pipeline: [
|
42 |
+
{
|
43 |
+
$match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" },
|
44 |
+
},
|
45 |
+
{
|
46 |
+
$group: {
|
47 |
+
_id: "$assistantId",
|
48 |
+
last24HoursCount: { $sum: "$count" },
|
49 |
},
|
50 |
+
},
|
51 |
+
],
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
},
|
53 |
+
},
|
54 |
+
{
|
55 |
+
$group: {
|
56 |
+
_id: "$_id",
|
57 |
+
last24HoursCount: { $sum: "$last24HoursCount" },
|
58 |
},
|
59 |
+
},
|
60 |
+
{
|
61 |
+
$merge: {
|
62 |
+
into: "assistants",
|
63 |
+
on: "_id",
|
64 |
+
whenMatched: "merge",
|
65 |
+
whenNotMatched: "discard",
|
66 |
},
|
67 |
+
},
|
68 |
+
])
|
69 |
+
.next();
|
70 |
+
})
|
71 |
+
);
|
72 |
} catch (e) {
|
73 |
logger.error(e, "Refresh assistants counter failed!");
|
74 |
}
|
src/lib/migrations/migrations.ts
CHANGED
@@ -13,7 +13,7 @@ export async function checkAndRunMigrations() {
|
|
13 |
}
|
14 |
|
15 |
// check if all migrations have already been run
|
16 |
-
const migrationResults = await Database.getInstance()
|
17 |
.getCollections()
|
18 |
.migrationResults.find()
|
19 |
.toArray();
|
@@ -21,7 +21,7 @@ export async function checkAndRunMigrations() {
|
|
21 |
logger.info("[MIGRATIONS] Begin check...");
|
22 |
|
23 |
// connect to the database
|
24 |
-
const connectedClient = await Database.getInstance().getClient().connect();
|
25 |
|
26 |
const lockId = await acquireLock(LOCK_KEY);
|
27 |
|
@@ -74,25 +74,23 @@ export async function checkAndRunMigrations() {
|
|
74 |
}. Applying...`
|
75 |
);
|
76 |
|
77 |
-
await Database.getInstance()
|
78 |
-
.
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
name: migration.name,
|
84 |
-
status: "ongoing",
|
85 |
-
},
|
86 |
},
|
87 |
-
|
88 |
-
|
|
|
89 |
|
90 |
const session = connectedClient.startSession();
|
91 |
let result = false;
|
92 |
|
93 |
try {
|
94 |
await session.withTransaction(async () => {
|
95 |
-
result = await migration.up(Database.getInstance());
|
96 |
});
|
97 |
} catch (e) {
|
98 |
logger.info(`[MIGRATIONS] "${migration.name}" failed!`);
|
@@ -101,18 +99,16 @@ export async function checkAndRunMigrations() {
|
|
101 |
await session.endSession();
|
102 |
}
|
103 |
|
104 |
-
await Database.getInstance()
|
105 |
-
.
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
name: migration.name,
|
111 |
-
status: result ? "success" : "failure",
|
112 |
-
},
|
113 |
},
|
114 |
-
|
115 |
-
|
|
|
116 |
}
|
117 |
}
|
118 |
|
|
|
13 |
}
|
14 |
|
15 |
// check if all migrations have already been run
|
16 |
+
const migrationResults = await (await Database.getInstance())
|
17 |
.getCollections()
|
18 |
.migrationResults.find()
|
19 |
.toArray();
|
|
|
21 |
logger.info("[MIGRATIONS] Begin check...");
|
22 |
|
23 |
// connect to the database
|
24 |
+
const connectedClient = await (await Database.getInstance()).getClient().connect();
|
25 |
|
26 |
const lockId = await acquireLock(LOCK_KEY);
|
27 |
|
|
|
74 |
}. Applying...`
|
75 |
);
|
76 |
|
77 |
+
await (await Database.getInstance()).getCollections().migrationResults.updateOne(
|
78 |
+
{ _id: migration._id },
|
79 |
+
{
|
80 |
+
$set: {
|
81 |
+
name: migration.name,
|
82 |
+
status: "ongoing",
|
|
|
|
|
|
|
83 |
},
|
84 |
+
},
|
85 |
+
{ upsert: true }
|
86 |
+
);
|
87 |
|
88 |
const session = connectedClient.startSession();
|
89 |
let result = false;
|
90 |
|
91 |
try {
|
92 |
await session.withTransaction(async () => {
|
93 |
+
result = await migration.up(await Database.getInstance());
|
94 |
});
|
95 |
} catch (e) {
|
96 |
logger.info(`[MIGRATIONS] "${migration.name}" failed!`);
|
|
|
99 |
await session.endSession();
|
100 |
}
|
101 |
|
102 |
+
await (await Database.getInstance()).getCollections().migrationResults.updateOne(
|
103 |
+
{ _id: migration._id },
|
104 |
+
{
|
105 |
+
$set: {
|
106 |
+
name: migration.name,
|
107 |
+
status: result ? "success" : "failure",
|
|
|
|
|
|
|
108 |
},
|
109 |
+
},
|
110 |
+
{ upsert: true }
|
111 |
+
);
|
112 |
}
|
113 |
}
|
114 |
|
src/lib/server/database.ts
CHANGED
@@ -14,29 +14,69 @@ import type { MigrationResult } from "$lib/types/MigrationResult";
|
|
14 |
import type { Semaphore } from "$lib/types/Semaphore";
|
15 |
import type { AssistantStats } from "$lib/types/AssistantStats";
|
16 |
import type { CommunityToolDB } from "$lib/types/Tool";
|
17 |
-
|
18 |
import { logger } from "$lib/server/logger";
|
19 |
import { building } from "$app/environment";
|
20 |
import type { TokenCache } from "$lib/types/TokenCache";
|
21 |
import { onExit } from "./exitHandler";
|
|
|
|
|
|
|
22 |
|
23 |
export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
export class Database {
|
26 |
-
private client
|
|
|
27 |
|
28 |
private static instance: Database;
|
29 |
|
30 |
-
private
|
31 |
if (!env.MONGODB_URL) {
|
32 |
-
|
33 |
-
"Please specify the MONGODB_URL environment variable inside .env.local. Set it to mongodb://localhost:27017 if you are running MongoDB locally, or to a MongoDB Atlas free instance for example."
|
34 |
-
);
|
35 |
-
}
|
36 |
|
37 |
-
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
|
41 |
this.client.connect().catch((err) => {
|
42 |
logger.error(err, "Connection error");
|
@@ -46,12 +86,17 @@ export class Database {
|
|
46 |
this.client.on("open", () => this.initDatabase());
|
47 |
|
48 |
// Disconnect DB on exit
|
49 |
-
onExit(() =>
|
|
|
|
|
|
|
|
|
50 |
}
|
51 |
|
52 |
-
public static getInstance(): Database {
|
53 |
if (!Database.instance) {
|
54 |
Database.instance = new Database();
|
|
|
55 |
}
|
56 |
|
57 |
return Database.instance;
|
@@ -61,6 +106,10 @@ export class Database {
|
|
61 |
* Return mongoClient
|
62 |
*/
|
63 |
public getClient(): MongoClient {
|
|
|
|
|
|
|
|
|
64 |
return this.client;
|
65 |
}
|
66 |
|
@@ -68,6 +117,10 @@ export class Database {
|
|
68 |
* Return map of database's collections
|
69 |
*/
|
70 |
public getCollections() {
|
|
|
|
|
|
|
|
|
71 |
const db = this.client.db(
|
72 |
env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")
|
73 |
);
|
@@ -247,4 +300,4 @@ export class Database {
|
|
247 |
|
248 |
export const collections = building
|
249 |
? ({} as unknown as ReturnType<typeof Database.prototype.getCollections>)
|
250 |
-
: Database.getInstance().getCollections();
|
|
|
14 |
import type { Semaphore } from "$lib/types/Semaphore";
|
15 |
import type { AssistantStats } from "$lib/types/AssistantStats";
|
16 |
import type { CommunityToolDB } from "$lib/types/Tool";
|
17 |
+
import { MongoMemoryServer } from "mongodb-memory-server";
|
18 |
import { logger } from "$lib/server/logger";
|
19 |
import { building } from "$app/environment";
|
20 |
import type { TokenCache } from "$lib/types/TokenCache";
|
21 |
import { onExit } from "./exitHandler";
|
22 |
+
import { fileURLToPath } from "url";
|
23 |
+
import { dirname, join } from "path";
|
24 |
+
import { existsSync, mkdirSync } from "fs";
|
25 |
|
26 |
export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
|
27 |
|
28 |
+
function findRepoRoot(startPath: string): string {
|
29 |
+
let currentPath = startPath;
|
30 |
+
while (currentPath !== "/") {
|
31 |
+
if (existsSync(join(currentPath, "package.json"))) {
|
32 |
+
return currentPath;
|
33 |
+
}
|
34 |
+
currentPath = dirname(currentPath);
|
35 |
+
}
|
36 |
+
throw new Error("Could not find repository root (no package.json found)");
|
37 |
+
}
|
38 |
+
|
39 |
export class Database {
|
40 |
+
private client?: MongoClient;
|
41 |
+
private mongoServer?: MongoMemoryServer;
|
42 |
|
43 |
private static instance: Database;
|
44 |
|
45 |
+
private async init() {
|
46 |
if (!env.MONGODB_URL) {
|
47 |
+
logger.warn("No MongoDB URL found, using in-memory server");
|
|
|
|
|
|
|
48 |
|
49 |
+
// Find repo root by looking for package.json
|
50 |
+
const currentFilePath = fileURLToPath(import.meta.url);
|
51 |
+
const repoRoot = findRepoRoot(dirname(currentFilePath));
|
52 |
+
|
53 |
+
// Use MONGO_STORAGE_PATH from env if set, otherwise use db/ in repo root
|
54 |
+
const dbPath = env.MONGO_STORAGE_PATH || join(repoRoot, "db");
|
55 |
+
|
56 |
+
logger.info(`Using database path: ${dbPath}`);
|
57 |
+
// Create db directory if it doesn't exist
|
58 |
+
if (!existsSync(dbPath)) {
|
59 |
+
logger.info(`Creating database directory at ${dbPath}`);
|
60 |
+
mkdirSync(dbPath, { recursive: true });
|
61 |
+
}
|
62 |
+
|
63 |
+
this.mongoServer = await MongoMemoryServer.create({
|
64 |
+
instance: {
|
65 |
+
dbName: env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""),
|
66 |
+
dbPath,
|
67 |
+
},
|
68 |
+
binary: {
|
69 |
+
version: "7.0.18",
|
70 |
+
},
|
71 |
+
});
|
72 |
+
this.client = new MongoClient(this.mongoServer.getUri(), {
|
73 |
+
directConnection: env.MONGODB_DIRECT_CONNECTION === "true",
|
74 |
+
});
|
75 |
+
} else {
|
76 |
+
this.client = new MongoClient(env.MONGODB_URL, {
|
77 |
+
directConnection: env.MONGODB_DIRECT_CONNECTION === "true",
|
78 |
+
});
|
79 |
+
}
|
80 |
|
81 |
this.client.connect().catch((err) => {
|
82 |
logger.error(err, "Connection error");
|
|
|
86 |
this.client.on("open", () => this.initDatabase());
|
87 |
|
88 |
// Disconnect DB on exit
|
89 |
+
onExit(async () => {
|
90 |
+
logger.info("Closing database connection");
|
91 |
+
await this.client?.close(true);
|
92 |
+
await this.mongoServer?.stop();
|
93 |
+
});
|
94 |
}
|
95 |
|
96 |
+
public static async getInstance(): Promise<Database> {
|
97 |
if (!Database.instance) {
|
98 |
Database.instance = new Database();
|
99 |
+
await Database.instance.init();
|
100 |
}
|
101 |
|
102 |
return Database.instance;
|
|
|
106 |
* Return mongoClient
|
107 |
*/
|
108 |
public getClient(): MongoClient {
|
109 |
+
if (!this.client) {
|
110 |
+
throw new Error("Database not initialized");
|
111 |
+
}
|
112 |
+
|
113 |
return this.client;
|
114 |
}
|
115 |
|
|
|
117 |
* Return map of database's collections
|
118 |
*/
|
119 |
public getCollections() {
|
120 |
+
if (!this.client) {
|
121 |
+
throw new Error("Database not initialized");
|
122 |
+
}
|
123 |
+
|
124 |
const db = this.client.db(
|
125 |
env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")
|
126 |
);
|
|
|
300 |
|
301 |
export const collections = building
|
302 |
? ({} as unknown as ReturnType<typeof Database.prototype.getCollections>)
|
303 |
+
: await Database.getInstance().then((db) => db.getCollections());
|
src/routes/assistants/+page.server.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import { base } from "$app/paths";
|
2 |
import { env } from "$env/dynamic/private";
|
3 |
-
import {
|
4 |
import { SortKey, type Assistant } from "$lib/types/Assistant";
|
5 |
import type { User } from "$lib/types/User";
|
6 |
import { generateQueryTokens } from "$lib/utils/searchTokens.js";
|
@@ -58,9 +58,8 @@ export const load = async ({ url, locals }) => {
|
|
58 |
...shouldBeFeatured,
|
59 |
};
|
60 |
|
61 |
-
const assistants = await
|
62 |
-
.
|
63 |
-
.assistants.find(filter)
|
64 |
.sort({
|
65 |
...(sort === SortKey.TRENDING && { last24HoursCount: -1 }),
|
66 |
userCount: -1,
|
@@ -70,9 +69,7 @@ export const load = async ({ url, locals }) => {
|
|
70 |
.limit(NUM_PER_PAGE)
|
71 |
.toArray();
|
72 |
|
73 |
-
const numTotalItems = await
|
74 |
-
.getCollections()
|
75 |
-
.assistants.countDocuments(filter);
|
76 |
|
77 |
return {
|
78 |
assistants: JSON.parse(JSON.stringify(assistants)) as Array<Assistant>,
|
|
|
1 |
import { base } from "$app/paths";
|
2 |
import { env } from "$env/dynamic/private";
|
3 |
+
import { collections } from "$lib/server/database.js";
|
4 |
import { SortKey, type Assistant } from "$lib/types/Assistant";
|
5 |
import type { User } from "$lib/types/User";
|
6 |
import { generateQueryTokens } from "$lib/utils/searchTokens.js";
|
|
|
58 |
...shouldBeFeatured,
|
59 |
};
|
60 |
|
61 |
+
const assistants = await collections.assistants
|
62 |
+
.find(filter)
|
|
|
63 |
.sort({
|
64 |
...(sort === SortKey.TRENDING && { last24HoursCount: -1 }),
|
65 |
userCount: -1,
|
|
|
69 |
.limit(NUM_PER_PAGE)
|
70 |
.toArray();
|
71 |
|
72 |
+
const numTotalItems = await collections.assistants.countDocuments(filter);
|
|
|
|
|
73 |
|
74 |
return {
|
75 |
assistants: JSON.parse(JSON.stringify(assistants)) as Array<Assistant>,
|
src/routes/tools/+page.server.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import { env } from "$env/dynamic/private";
|
2 |
import { authCondition } from "$lib/server/auth.js";
|
3 |
-
import {
|
4 |
import { toolFromConfigs } from "$lib/server/tools/index.js";
|
5 |
import { SortKey } from "$lib/types/Assistant.js";
|
6 |
import { ReviewStatus } from "$lib/types/Review";
|
@@ -60,9 +60,8 @@ export const load = async ({ url, locals }) => {
|
|
60 |
}),
|
61 |
};
|
62 |
|
63 |
-
const communityTools = await
|
64 |
-
.
|
65 |
-
.tools.find(filter)
|
66 |
.skip(NUM_PER_PAGE * pageIndex)
|
67 |
.sort({
|
68 |
...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }),
|
@@ -84,9 +83,7 @@ export const load = async ({ url, locals }) => {
|
|
84 |
|
85 |
const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools];
|
86 |
|
87 |
-
const numTotalItems =
|
88 |
-
(await Database.getInstance().getCollections().tools.countDocuments(filter)) +
|
89 |
-
toolFromConfigs.length;
|
90 |
|
91 |
return {
|
92 |
tools: JSON.parse(JSON.stringify(tools)) as CommunityToolDB[],
|
|
|
1 |
import { env } from "$env/dynamic/private";
|
2 |
import { authCondition } from "$lib/server/auth.js";
|
3 |
+
import { collections } from "$lib/server/database.js";
|
4 |
import { toolFromConfigs } from "$lib/server/tools/index.js";
|
5 |
import { SortKey } from "$lib/types/Assistant.js";
|
6 |
import { ReviewStatus } from "$lib/types/Review";
|
|
|
60 |
}),
|
61 |
};
|
62 |
|
63 |
+
const communityTools = await collections.tools
|
64 |
+
.find(filter)
|
|
|
65 |
.skip(NUM_PER_PAGE * pageIndex)
|
66 |
.sort({
|
67 |
...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }),
|
|
|
83 |
|
84 |
const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools];
|
85 |
|
86 |
+
const numTotalItems = (await collections.tools.countDocuments(filter)) + toolFromConfigs.length;
|
|
|
|
|
87 |
|
88 |
return {
|
89 |
tools: JSON.parse(JSON.stringify(tools)) as CommunityToolDB[],
|