Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mongodb driver migration and graceful process shutdown #124

Merged
merged 4 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ RUN apt-get install unzip
FROM scaffold AS cache
RUN deno cache src/index.ts

# Complilation broken in the latest version of deno
# FROM cache AS final
# RUN deno compile --allow-net --allow-env --allow-read --output=server src/index.ts
# Fallback and compilation has broken in some instances
#ENTRYPOINT ["deno", "run", "--allow-net", "--allow-env", "--allow-read", "--allow-sys", "src/index.ts"]

#ENTRYPOINT ["/usr/app/server"]
FROM cache AS build
RUN deno check src/index.ts
RUN deno compile --allow-net --allow-env --allow-read --allow-sys --output=/usr/on-the-edge src/index.ts

ENTRYPOINT ["deno", "run", "--allow-net", "--allow-env", "--allow-read", "src/index.ts"]
FROM build AS final
RUN rm -r /usr/app
WORKDIR /usr

ENTRYPOINT ["/usr/on-the-edge"]
26 changes: 10 additions & 16 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,23 @@
"$schema": "http://json-schema.org/draft-07/schema",
"importMap": "./src/import_map.json",
"lint": {
"files": {
"include": ["src"],
"exclude": [".github", "README.md"]
},
"include": ["src"],
"exclude": [".github", "README.md"],
"rules": {
"tags": ["recommended"]
}
},
"fmt": {
"files": {
"include": ["src"],
"exclude": [".github", "README.md"]
},
"options": {
"useTabs": false,
"lineWidth": 80,
"indentWidth": 2,
"singleQuote": true,
"proseWrap": "preserve"
}
"include": ["src"],
"exclude": [".github", "README.md"],
"useTabs": false,
"lineWidth": 80,
"indentWidth": 2,
"singleQuote": true,
"proseWrap": "preserve"
},
"tasks": {
"test": "deno test --allow-read --allow-env --allow-net",
"server": "deno run --allow-read --allow-env --allow-net ./src/index.ts"
"server": "deno run --allow-read --allow-env --allow-net --allow-sys ./src/index.ts"
}
}
155 changes: 155 additions & 0 deletions deno.lock

Large diffs are not rendered by default.

40 changes: 15 additions & 25 deletions src/common/core/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,30 @@ import header from '../middleware/header.ts';
import targeting from '../middleware/targeting.ts';
import { logger } from './logger.ts';
import { between } from 'x/optic';
import _localSourceFactory from '../mongo/factory.ts';

const app = new Application({
state,
contextState: 'prototype',
});

export default (opts: FactoryOptions): Application => {
logger.mark('factory-start');
const router = opts.router ?? new Router();
app.use(
timing,
header,
growth,
targeting,
error,
);

app.use(
timing,
header,
growth,
targeting,
error,
app.addEventListener('error', (event) => {
logger.critical(
'common.core.factory:error: Uncaught application exception',
event.error,
);
});

app.addEventListener('close', (event) => {
_localSourceFactory.disconnect();
logger.info(
'common:core:factory:close: Request application stop by user',
event.type,
);
});

app.addEventListener('error', (event) => {
_localSourceFactory.disconnect();
logger.critical(
'common.core.factory:error: Uncaught application exception',
event.error,
);
});
export default (opts: FactoryOptions): Application => {
logger.mark('factory-start');
const router = opts.router ?? new Router();

app.use(router.routes());
app.use(router.allowedMethods());
Expand Down
27 changes: 17 additions & 10 deletions src/common/core/request.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { between } from 'x/optic';
import { logger } from './logger.ts';

const sanitize = (url: string): string => {
const urlObject = new URL(url);
const sanitize = (uri: string): { safeUrl: string; host: string } => {
const url = new URL(uri);

const queryParams = urlObject.searchParams;
const queryParams = url.searchParams;
for (const key of queryParams.keys()) {
if (key.includes('api_key') || key.includes('api_secret')) {
queryParams.set(key, '********');
}
}

return urlObject.toString();
return { safeUrl: url.toString(), host: url.host };
};

export const defaults: RequestInit = {
Expand All @@ -20,23 +20,30 @@ export const defaults: RequestInit = {
headers: {
'accept': 'application/json, application/xml, text/plain, */*',
'accept-encoding': 'gzip, deflate, br',
'connection': 'keep-alive',
'user-agent': `Deno/${Deno.version.deno}`,
},
};

export const request = async <T>(
url: string,
options: RequestInit = defaults,
): Promise<T> => {
const sanitizedUrl = sanitize(url);
logger.debug(`----> ${options.method}: ${sanitizedUrl}`);
logger.mark('request-start');
return await fetch(url, options).then((response) => {
logger.debug(`<---- ${response.status} ${options.method}: ${sanitizedUrl}`);
const { safeUrl, host } = sanitize(url);
logger.debug(`----> HTTP ${options.method}: ${safeUrl}`);
return await fetch(url, {
headers: {
...options.headers,
host: host,
},
}).then((response) => {
logger.debug(`<---- HTTP/${response.status} ${options.method}: ${safeUrl}`);
logger.mark('request-end');
logger.measure(between('request-start', 'request-end'), sanitizedUrl);
logger.measure(between('request-start', 'request-end'), host);
if (!response.ok) {
throw new Error(
`<---- HTTP/${response.status} ${options.method}: ${sanitizedUrl}`,
`<---- HTTP/${response.status} ${options.method}: ${safeUrl}`,
);
}
const contentType = response.headers.get('Content-Type');
Expand Down
25 changes: 24 additions & 1 deletion src/common/core/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,34 @@ const applicationState: State = {
authorization: null,
contentType: null,
acceptEncoding: '',
language: '',
},
local: await _localSourceFactory.connect(),
};

const onDispose = (token: number) => {
setTimeout(() => {
Deno.removeSignalListener('SIGINT', onTerminationRequest);
Deno.removeSignalListener('SIGTERM', onTerminationRequest);
clearTimeout(token);
Deno.exit();
}, 500);
};

const onTerminationRequest = (): void => {
logger.debug(
'common.core.setup:onTerminationRequest: OS dispatched signal',
);
const token = setTimeout(async () => await _localSourceFactory.disconnect());
logger.debug(
'common.core.setup:onTerminationRequest: Attempting to exit Deno process',
);

onDispose(token);
};

Deno.addSignalListener('SIGINT', onTerminationRequest);
Deno.addSignalListener('SIGTERM', onTerminationRequest);

logger.mark('setup-end');
logger.measure(between('setup-start', 'setup-end'));

Expand Down
3 changes: 3 additions & 0 deletions src/common/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ export const pagination = (page: number, count: number) => {
return { from, to };
};

export const isNullOrUndefined = (obj: unknown) =>
obj == null || obj == undefined;

export const port = env<number>('PORT');
5 changes: 2 additions & 3 deletions src/common/middleware/targeting.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { between } from 'x/optic';
import { State } from '../types/state.d.ts';
import { UAParser } from 'esm/ua_parser';
import { UserAgent } from 'std/http';
import { logger } from '../core/logger.ts';
import type { AppContext } from '../types/core.d.ts';

Expand All @@ -22,8 +22,7 @@ const contextAttributes = (
};
}

const parser = new UAParser(agent);
const { browser, cpu, device, engine, os } = parser.getResult();
const { browser, cpu, device, engine, os } = new UserAgent(agent);

return Promise.resolve({
'browser_name': browser.name,
Expand Down
8 changes: 4 additions & 4 deletions src/common/mongo/collection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Document } from 'x/mongo';
import { Document } from 'npm/mongodb';
import { Local } from '../types/core.d.ts';

export const getCollection = <T extends Document>(
collection: string,
export const collection = <T extends Document>(
name: string,
database: Local,
) => database?.collection<T>(collection);
) => database?.collection<T>(name);
58 changes: 32 additions & 26 deletions src/common/mongo/factory.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
import { MongoClient } from 'x/mongo';
import { MongoClient } from 'npm/mongodb';
import { logger } from '../core/logger.ts';
import { between } from 'x/optic';
import { env } from '../core/env.ts';
import { Local } from '../types/core.d.ts';

class LocalSourceFactory {
constructor(
private readonly options: string,
private readonly client: MongoClient,
) {
constructor(private readonly client: MongoClient) {
logger.mark('mongo_connection_start');
client.on('timeout', () => {
logger.warn(
'common.mongo.factory:LocalSourceFactory: Connection timed out',
);
});
}

connect = async (): Promise<Local> => {
try {
const db = await this.client.connect(this.options);
logger.mark('mongo_connection_end');
logger.measure(
between('mongo_connection_start', 'mongo_connection_end'),
);
return db;
} catch (e) {
logger.error('common.mongo.factory:connect:', e);
return undefined;
}
return await this.client.connect()
.then((client) => {
logger.mark('mongo_connection_end');
logger.measure(
between('mongo_connection_start', 'mongo_connection_end'),
);
return client.db();
})
.catch((e) => {
logger.error('common.mongo.factory:connect:', e);
return undefined;
});
};

disconnect = () => {
disconnect = async () => {
logger.mark('mongo_close_start');
try {
this.client.close();
} catch (e) {
logger.error('common.mongo.factory:disconnect:', e);
}
logger.mark('mongo_close_end');
logger.measure(between('mongo_close_start', 'mongo_close_end'));
await this.client.close(true)
.then(() => {
}).catch((e) => {
logger.error('common.mongo.factory:disconnect:', e);
}).finally(() => {
logger.mark('mongo_close_end');
logger.measure(between('mongo_close_start', 'mongo_close_end'));
});
};
}

const _localSourceFactory = new LocalSourceFactory(
env<string>('MONGO_URL'),
new MongoClient(),
new MongoClient(env<string>('MONGO_URL'), {
connectTimeoutMS: 1000,
monitorCommands: true,
}),
);

export default _localSourceFactory;
2 changes: 2 additions & 0 deletions src/common/mongo/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './collection.ts';
export * from './utils.ts';
export * from './types.d.ts';
15 changes: 15 additions & 0 deletions src/common/mongo/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Document, OptionalId, SortDirection } from 'npm/mongodb';

export type Optional<T extends Document> = T | undefined | null;

export type ProjectionOption<T extends Document> = {
[K in keyof OptionalId<T>]?: 0 | 1;
};

export type SortOption<T extends Document> = {
[K in keyof OptionalId<T>]?: SortDirection;
};

export interface EntityCursor {
cursor: string;
}
11 changes: 11 additions & 0 deletions src/common/mongo/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Document, ObjectId, Sort } from 'npm/mongodb';
import { ProjectionOption, SortOption } from './types.d.ts';

export const projectionOf = <T extends Document>(
projection: ProjectionOption<T>,
) => projection;

export const sortOf = <T extends Document>(option: SortOption<T>): Sort =>
option as Sort;

export const idOf = (id: ObjectId): string => id.toHexString();
4 changes: 2 additions & 2 deletions src/common/types/core.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context } from 'x/oak';
import { State } from './state.d.ts';
import { GrowthBook } from 'esm/growthbook';
import { Database } from 'x/mongo';
import { Db } from 'npm/mongodb';
import { AppFeatures } from '../experiment/types.d.ts';

export type RCF822Date = string;
Expand All @@ -14,4 +14,4 @@ export type AppContext = Context<State>;

export type Features = GrowthBook<AppFeatures>;

export type Local = Database | undefined;
export type Local = Db | undefined;
4 changes: 2 additions & 2 deletions src/common/types/paging.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IResponse } from './response.d.ts';

export interface IPaging<T> extends IResponse<T[]> {
page: number;
first?: string;
last?: string;
count: number;
total: number;
}
4 changes: 2 additions & 2 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AppContext } from '../common/types/core.d.ts';
import { Repository } from './repository/index.ts';
import { LocalSource } from './local/index.ts';
import { getCollection } from '../common/mongo/index.ts';
import { collection } from '../common/mongo/index.ts';

export const config = async ({ state, response }: AppContext) => {
const localSource = new LocalSource(getCollection('config', state.local));
const localSource = new LocalSource(collection('config', state.local));
const repository = new Repository(
state.features,
localSource,
Expand Down
Loading
Loading