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(NODE-6391): Add timeoutMS support to explicit encryption #4269

Merged
merged 70 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
8003163
feat(NODE-6090): Implement CSOT logic for connection checkout and ser…
W-A-James Apr 11, 2024
a216ae6
test(NODE-6120): Implement Unified test runner changes for CSOT (#4121)
W-A-James Jun 10, 2024
aca9661
refactor(NODE-6187): refactor to use TimeoutContext abstraction (#4131)
W-A-James Jun 21, 2024
3051def
refactor(NODE-6230): executeOperation to use iterative retry mechanis…
nbbeeken Jul 22, 2024
df025f4
feat(NODE-5682): set maxTimeMS on commands and preempt I/O (#4174)
nbbeeken Jul 26, 2024
83cd82b
feat(NODE-6231): Add CSOT behaviour for retryable reads and writes (#…
W-A-James Aug 1, 2024
c36dce5
feat(NODE-6312): add error transformation for server timeouts (#4192)
nbbeeken Aug 12, 2024
3fe3e01
feat(NODE-6313): add CSOT support to sessions and transactions (#4199)
nbbeeken Sep 9, 2024
7b4aa84
feat(NODE-6304): add CSOT support for non-tailable cursors (#4195)
W-A-James Sep 12, 2024
3045a34
fix(NODE-6374): MongoOperationTimeoutError inherits MongoRuntimeError…
nbbeeken Sep 12, 2024
11d059f
test: remove empty skipped context blocks (#4238)
W-A-James Sep 12, 2024
bfeeda9
feat(NODE-5844): add iscryptd to ServerDescription (#4239)
nbbeeken Sep 17, 2024
7a12914
chore: allow clientBulkWrite to use TimeoutContext (#4251)
W-A-James Sep 25, 2024
09f6d7d
feat(NODE-6274): add CSOT support to bulkWrite (#4250)
nbbeeken Oct 2, 2024
1a06868
feat(NODE-6275): Add CSOT support to GridFS (#4246)
W-A-James Oct 4, 2024
392599c
refactor(NODE-6411): AbstractCursor accepts an external timeout conte…
baileympearson Oct 4, 2024
9a1b2d0
feat(NODE-6305): Add CSOT support to tailable cursors (#4218)
W-A-James Oct 7, 2024
d26a588
feat(NODE-6389): add support for timeoutMS in StateMachine.execute() …
aditi-khare-mongoDB Oct 7, 2024
2206be1
src code change no tests
aditi-khare-mongoDB Oct 8, 2024
6330fd6
feat(NODE-6090): Implement CSOT logic for connection checkout and ser…
W-A-James Apr 11, 2024
a1206a0
test(NODE-6120): Implement Unified test runner changes for CSOT (#4121)
W-A-James Jun 10, 2024
a47e280
refactor(NODE-6187): refactor to use TimeoutContext abstraction (#4131)
W-A-James Jun 21, 2024
398066e
refactor(NODE-6230): executeOperation to use iterative retry mechanis…
nbbeeken Jul 22, 2024
c333723
feat(NODE-5682): set maxTimeMS on commands and preempt I/O (#4174)
nbbeeken Jul 26, 2024
256ca4e
feat(NODE-6231): Add CSOT behaviour for retryable reads and writes (#…
W-A-James Aug 1, 2024
8a416be
feat(NODE-6312): add error transformation for server timeouts (#4192)
nbbeeken Aug 12, 2024
52c2c9d
feat(NODE-6313): add CSOT support to sessions and transactions (#4199)
nbbeeken Sep 9, 2024
546366f
feat(NODE-6304): add CSOT support for non-tailable cursors (#4195)
W-A-James Sep 12, 2024
4f8e7c9
fix(NODE-6374): MongoOperationTimeoutError inherits MongoRuntimeError…
nbbeeken Sep 12, 2024
8b9eeef
test: remove empty skipped context blocks (#4238)
W-A-James Sep 12, 2024
1eb0b74
feat(NODE-5844): add iscryptd to ServerDescription (#4239)
nbbeeken Sep 17, 2024
580130d
chore: allow clientBulkWrite to use TimeoutContext (#4251)
W-A-James Sep 25, 2024
2e93ce7
feat(NODE-6274): add CSOT support to bulkWrite (#4250)
nbbeeken Oct 2, 2024
c637ea8
feat(NODE-6275): Add CSOT support to GridFS (#4246)
W-A-James Oct 4, 2024
c148f6b
refactor(NODE-6411): AbstractCursor accepts an external timeout conte…
baileympearson Oct 4, 2024
4488bab
feat(NODE-6305): Add CSOT support to tailable cursors (#4218)
W-A-James Oct 7, 2024
c28a365
feat(NODE-6389): add support for timeoutMS in StateMachine.execute() …
aditi-khare-mongoDB Oct 7, 2024
85d39ec
fix(NODE-6412): read stale response from previously timed out connect…
nbbeeken Oct 11, 2024
450b163
feat(NODE-6403): add CSOT support to client bulk write (#4261)
baileympearson Oct 14, 2024
35ee04c
test 1
aditi-khare-mongoDB Oct 15, 2024
7ee1fd2
tests implemented
aditi-khare-mongoDB Oct 15, 2024
dfe72c1
Merge branch 'NODE-6090' into NODE-6391/explicit-encryption
aditi-khare-mongoDB Oct 15, 2024
56c63c7
temp
aditi-khare-mongoDB Oct 15, 2024
fff7e0a
temp
aditi-khare-mongoDB Oct 15, 2024
751ecd1
temp
aditi-khare-mongoDB Oct 16, 2024
ea2089a
temp
aditi-khare-mongoDB Oct 17, 2024
fa05342
temp
aditi-khare-mongoDB Oct 17, 2024
cbb2a56
temp
aditi-khare-mongoDB Oct 17, 2024
313eaa0
feat(NODE-6403): add CSOT support to client bulk write (#4261)
baileympearson Oct 14, 2024
07cffc7
chore: fix a few flaky CSOT tests (#4278)
baileympearson Oct 17, 2024
c3f31da
feat(NODE-6421): add support for timeoutMS to explain helpers (#4268)
baileympearson Oct 21, 2024
95dd2a2
ready for review
aditi-khare-mongoDB Oct 21, 2024
cf606a0
Merge branch 'NODE-6090' into NODE-6391/explicit-encryption
aditi-khare-mongoDB Oct 21, 2024
3abd62b
remove extranous changes
aditi-khare-mongoDB Oct 21, 2024
c424c80
add back in tests from rebase
aditi-khare-mongoDB Oct 21, 2024
e78b127
Merge branch 'NODE-6090' into NODE-6391/explicit-encryption
aditi-khare-mongoDB Oct 21, 2024
19c314f
partial re-review
aditi-khare-mongoDB Oct 24, 2024
3587432
requested changes
aditi-khare-mongoDB Oct 24, 2024
abe248f
no concurrent timeoutContext
aditi-khare-mongoDB Oct 24, 2024
3fbcd2e
add in comments
aditi-khare-mongoDB Oct 24, 2024
ad8970a
typo
aditi-khare-mongoDB Oct 24, 2024
9201a08
Update src/client-side-encryption/client_encryption.ts
aditi-khare-mongoDB Oct 24, 2024
8287029
Update src/timeout.ts
aditi-khare-mongoDB Oct 24, 2024
cab26a2
bailey requested changes
aditi-khare-mongoDB Oct 28, 2024
cddad21
bailey requested changes
aditi-khare-mongoDB Oct 28, 2024
2fb01bc
fix merge conflict
aditi-khare-mongoDB Oct 28, 2024
171c766
Merge branch 'NODE-6090' into NODE-6391/explicit-encryption
aditi-khare-mongoDB Oct 28, 2024
7185be8
fixed failing tests
aditi-khare-mongoDB Oct 28, 2024
c777bb3
Merge branch 'NODE-6090' into NODE-6391/explicit-encryption
aditi-khare-mongoDB Oct 29, 2024
12333d1
lint fix
aditi-khare-mongoDB Oct 29, 2024
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
Prev Previous commit
Next Next commit
refactor(NODE-6187): refactor to use TimeoutContext abstraction (#4131)
  • Loading branch information
W-A-James authored and nbbeeken committed Oct 14, 2024
commit a47e28061160bf26c8121c9a502839bd85546f06
4 changes: 4 additions & 0 deletions src/bulk/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { makeUpdateStatement, UpdateOperation, type UpdateStatement } from '../o
import type { Server } from '../sdam/server';
import type { Topology } from '../sdam/topology';
import type { ClientSession } from '../sessions';
import { type TimeoutContext } from '../timeout';
import {
applyRetryableWrites,
getTopology,
Expand Down Expand Up @@ -842,6 +843,9 @@ export interface BulkWriteOptions extends CommandOperationOptions {
forceServerObjectId?: boolean;
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
let?: Document;

/** @internal */
timeoutContext?: TimeoutContext;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/cmap/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { type CancellationToken, TypedEventEmitter } from '../mongo_types';
import { ReadPreference, type ReadPreferenceLike } from '../read_preference';
import { ServerType } from '../sdam/common';
import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions';
import { type Timeout } from '../timeout';
import { type TimeoutContext } from '../timeout';
import {
BufferPool,
calculateDurationInMs,
Expand Down Expand Up @@ -97,7 +97,7 @@ export interface CommandOptions extends BSONSerializeOptions {
directConnection?: boolean;

/** @internal */
timeout?: Timeout;
timeoutContext?: TimeoutContext;
}

/** @public */
Expand Down
39 changes: 7 additions & 32 deletions src/cmap/connection_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import {
} from '../error';
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
import type { Server } from '../sdam/server';
import { Timeout, TimeoutError } from '../timeout';
import { type Callback, csotMin, List, makeCounter, promiseWithResolvers } from '../utils';
import { type TimeoutContext, TimeoutError } from '../timeout';
import { type Callback, List, makeCounter, promiseWithResolvers } from '../utils';
import { connect } from './connect';
import { Connection, type ConnectionEvents, type ConnectionOptions } from './connection';
import {
Expand Down Expand Up @@ -355,41 +355,15 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
* will be held by the pool. This means that if a connection is checked out it MUST be checked back in or
* explicitly destroyed by the new owner.
*/
async checkOut(options?: { timeout?: Timeout }): Promise<Connection> {
async checkOut(options: { timeoutContext: TimeoutContext }): Promise<Connection> {
this.emitAndLog(
ConnectionPool.CONNECTION_CHECK_OUT_STARTED,
new ConnectionCheckOutStartedEvent(this)
);

const waitQueueTimeoutMS = this.options.waitQueueTimeoutMS;
const serverSelectionTimeoutMS = this[kServer].topology.s.serverSelectionTimeoutMS;

const { promise, resolve, reject } = promiseWithResolvers<Connection>();

let timeout: Timeout | null = null;
if (options?.timeout) {
// CSOT enabled
// Determine if we're using the timeout passed in or a new timeout
if (options.timeout.duration > 0 || serverSelectionTimeoutMS > 0) {
// This check determines whether or not Topology.selectServer used the configured
// `timeoutMS` or `serverSelectionTimeoutMS` value for its timeout
if (
options.timeout.duration === serverSelectionTimeoutMS ||
csotMin(options.timeout.duration, serverSelectionTimeoutMS) < serverSelectionTimeoutMS
) {
// server selection used `timeoutMS`, so we should use the existing timeout as the timeout
// here
timeout = options.timeout;
} else {
// server selection used `serverSelectionTimeoutMS`, so we construct a new timeout with
// the time remaining to ensure that Topology.selectServer and ConnectionPool.checkOut
// cumulatively don't spend more than `serverSelectionTimeoutMS` blocking
timeout = Timeout.expires(serverSelectionTimeoutMS - options.timeout.timeElapsed);
}
}
} else {
timeout = Timeout.expires(waitQueueTimeoutMS);
}
const timeout = options.timeoutContext.connectionCheckoutTimeout;

const waitQueueMember: WaitQueueMember = {
resolve,
Expand All @@ -404,6 +378,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
return await (timeout ? Promise.race([promise, timeout]) : promise);
} catch (error) {
if (TimeoutError.is(error)) {
timeout?.clear();
waitQueueMember[kCancelled] = true;

this.emitAndLog(
Expand All @@ -416,7 +391,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
: 'Timed out while checking out a connection from connection pool',
this.address
);
if (options?.timeout) {
if (options.timeoutContext.csotEnabled()) {
throw new MongoOperationTimeoutError('Timed out during connection checkout', {
cause: timeoutError
});
Expand All @@ -425,7 +400,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
}
throw error;
} finally {
if (timeout !== options?.timeout) timeout?.clear();
if (options.timeoutContext.clearConnectionCheckoutTimeout) timeout?.clear();
}
}

Expand Down
18 changes: 16 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,13 @@ export type {
RTTSampler,
ServerMonitoringMode
} from './sdam/monitor';
export type { Server, ServerEvents, ServerOptions, ServerPrivate } from './sdam/server';
export type {
Server,
ServerCommandOptions,
ServerEvents,
ServerOptions,
ServerPrivate
} from './sdam/server';
export type {
ServerDescription,
ServerDescriptionOptions,
Expand Down Expand Up @@ -597,7 +603,15 @@ export type {
WithTransactionCallback
} from './sessions';
export type { Sort, SortDirection, SortDirectionForCmd, SortForCmd } from './sort';
export type { Timeout } from './timeout';
export type {
CSOTTimeoutContext,
CSOTTimeoutContextOptions,
LegacyTimeoutContext,
LegacyTimeoutContextOptions,
Timeout,
TimeoutContext,
TimeoutContextOptions
} from './timeout';
export type { Transaction, TransactionOptions, TxnState } from './transactions';
export type {
BufferPool,
Expand Down
5 changes: 4 additions & 1 deletion src/operations/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MongoInvalidArgumentError } from '../error';
import { type ExplainOptions } from '../explain';
import type { Server } from '../sdam/server';
import type { ClientSession } from '../sessions';
import { type TimeoutContext } from '../timeout';
import { maxWireVersion, type MongoDBNamespace } from '../utils';
import { WriteConcern } from '../write_concern';
import { type CollationOptions, CommandOperation, type CommandOperationOptions } from './command';
Expand Down Expand Up @@ -105,7 +106,8 @@ export class AggregateOperation extends CommandOperation<CursorResponse> {

override async execute(
server: Server,
session: ClientSession | undefined
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<CursorResponse> {
const options: AggregateOptions = this.options;
const serverWireVersion = maxWireVersion(server);
Expand Down Expand Up @@ -150,6 +152,7 @@ export class AggregateOperation extends CommandOperation<CursorResponse> {
server,
session,
command,
timeoutContext,
this.explain ? ExplainedCursorResponse : CursorResponse
);
}
Expand Down
11 changes: 9 additions & 2 deletions src/operations/bulk_write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
import type { Collection } from '../collection';
import type { Server } from '../sdam/server';
import type { ClientSession } from '../sessions';
import { type TimeoutContext } from '../timeout';
import { AbstractOperation, Aspect, defineAspects } from './operation';

/** @internal */
Expand All @@ -32,11 +33,17 @@ export class BulkWriteOperation extends AbstractOperation<BulkWriteResult> {

override async execute(
server: Server,
session: ClientSession | undefined
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<BulkWriteResult> {
const coll = this.collection;
const operations = this.operations;
const options = { ...this.options, ...this.bsonOptions, readPreference: this.readPreference };
const options = {
...this.options,
...this.bsonOptions,
readPreference: this.readPreference,
timeoutContext
};

// Create the bulk operation
const bulk: BulkOperationBase =
Expand Down
8 changes: 6 additions & 2 deletions src/operations/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ReadPreference } from '../read_preference';
import type { Server } from '../sdam/server';
import { MIN_SECONDARY_WRITE_WIRE_VERSION } from '../sdam/server_selection';
import type { ClientSession } from '../sessions';
import { type TimeoutContext } from '../timeout';
import {
commandSupportsReadConcern,
decorateWithExplain,
Expand Down Expand Up @@ -112,27 +113,30 @@ export abstract class CommandOperation<T> extends AbstractOperation<T> {
server: Server,
session: ClientSession | undefined,
cmd: Document,
timeoutContext: TimeoutContext,
responseType: T | undefined
): Promise<typeof responseType extends undefined ? Document : InstanceType<T>>;

public async executeCommand(
server: Server,
session: ClientSession | undefined,
cmd: Document
cmd: Document,
timeoutContext: TimeoutContext
): Promise<Document>;

async executeCommand(
server: Server,
session: ClientSession | undefined,
cmd: Document,
timeoutContext: TimeoutContext,
responseType?: MongoDBResponseConstructor
): Promise<Document> {
this.server = server;

const options = {
...this.options,
...this.bsonOptions,
timeout: this.timeout,
timeoutContext,
readPreference: this.readPreference,
session
};
Expand Down
9 changes: 7 additions & 2 deletions src/operations/count.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Document } from '../bson';
import type { Collection } from '../collection';
import type { Server } from '../sdam/server';
import type { ClientSession } from '../sessions';
import { type TimeoutContext } from '../timeout';
import type { MongoDBNamespace } from '../utils';
import { CommandOperation, type CommandOperationOptions } from './command';
import { Aspect, defineAspects } from './operation';
Expand Down Expand Up @@ -36,7 +37,11 @@ export class CountOperation extends CommandOperation<number> {
return 'count' as const;
}

override async execute(server: Server, session: ClientSession | undefined): Promise<number> {
override async execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<number> {
const options = this.options;
const cmd: Document = {
count: this.collectionName,
Expand All @@ -59,7 +64,7 @@ export class CountOperation extends CommandOperation<number> {
cmd.maxTimeMS = options.maxTimeMS;
}

const result = await super.executeCommand(server, session, cmd);
const result = await super.executeCommand(server, session, cmd, timeoutContext);
return result ? result.n : 0;
}
}
Expand Down
18 changes: 12 additions & 6 deletions src/operations/create_collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MongoCompatibilityError } from '../error';
import type { PkFactory } from '../mongo_client';
import type { Server } from '../sdam/server';
import type { ClientSession } from '../sessions';
import { type TimeoutContext } from '../timeout';
import { CommandOperation, type CommandOperationOptions } from './command';
import { CreateIndexesOperation } from './indexes';
import { Aspect, defineAspects } from './operation';
Expand Down Expand Up @@ -124,7 +125,11 @@ export class CreateCollectionOperation extends CommandOperation<Collection> {
return 'create' as const;
}

override async execute(server: Server, session: ClientSession | undefined): Promise<Collection> {
override async execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<Collection> {
const db = this.db;
const name = this.name;
const options = this.options;
Expand Down Expand Up @@ -155,15 +160,15 @@ export class CreateCollectionOperation extends CommandOperation<Collection> {
unique: true
}
});
await createOp.executeWithoutEncryptedFieldsCheck(server, session);
await createOp.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext);
}

if (!options.encryptedFields) {
this.options = { ...this.options, encryptedFields };
}
}

const coll = await this.executeWithoutEncryptedFieldsCheck(server, session);
const coll = await this.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext);

if (encryptedFields) {
// Create the required index for queryable encryption support.
Expand All @@ -173,15 +178,16 @@ export class CreateCollectionOperation extends CommandOperation<Collection> {
{ __safeContent__: 1 },
{}
);
await createIndexOp.execute(server, session);
await createIndexOp.execute(server, session, timeoutContext);
}

return coll;
}

private async executeWithoutEncryptedFieldsCheck(
server: Server,
session: ClientSession | undefined
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<Collection> {
const db = this.db;
const name = this.name;
Expand All @@ -198,7 +204,7 @@ export class CreateCollectionOperation extends CommandOperation<Collection> {
}
}
// otherwise just execute the command
await super.executeCommand(server, session, cmd);
await super.executeCommand(server, session, cmd, timeoutContext);
return new Collection(db, name, options);
}
}
Expand Down
21 changes: 15 additions & 6 deletions src/operations/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MongoCompatibilityError, MongoServerError } from '../error';
import { type TODO_NODE_3286 } from '../mongo_types';
import type { Server } from '../sdam/server';
import type { ClientSession } from '../sessions';
import { type TimeoutContext } from '../timeout';
import { type MongoDBNamespace } from '../utils';
import { type WriteConcernOptions } from '../write_concern';
import { type CollationOptions, CommandOperation, type CommandOperationOptions } from './command';
Expand Down Expand Up @@ -67,7 +68,8 @@ export class DeleteOperation extends CommandOperation<DeleteResult> {

override async execute(
server: Server,
session: ClientSession | undefined
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<DeleteResult> {
const options = this.options ?? {};
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
Expand Down Expand Up @@ -95,7 +97,12 @@ export class DeleteOperation extends CommandOperation<DeleteResult> {
}
}

const res: TODO_NODE_3286 = await super.executeCommand(server, session, command);
const res: TODO_NODE_3286 = await super.executeCommand(
server,
session,
command,
timeoutContext
);
return res;
}
}
Expand All @@ -107,9 +114,10 @@ export class DeleteOneOperation extends DeleteOperation {

override async execute(
server: Server,
session: ClientSession | undefined
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<DeleteResult> {
const res: TODO_NODE_3286 = await super.execute(server, session);
const res: TODO_NODE_3286 = await super.execute(server, session, timeoutContext);
if (this.explain) return res;
if (res.code) throw new MongoServerError(res);
if (res.writeErrors) throw new MongoServerError(res.writeErrors[0]);
Expand All @@ -127,9 +135,10 @@ export class DeleteManyOperation extends DeleteOperation {

override async execute(
server: Server,
session: ClientSession | undefined
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<DeleteResult> {
const res: TODO_NODE_3286 = await super.execute(server, session);
const res: TODO_NODE_3286 = await super.execute(server, session, timeoutContext);
if (this.explain) return res;
if (res.code) throw new MongoServerError(res);
if (res.writeErrors) throw new MongoServerError(res.writeErrors[0]);
Expand Down
Loading