Skip to content
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
1 change: 1 addition & 0 deletions packages/compass-schema/src/modules/schema-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const analyzeSchema = async (
},
{
abortSignal,
fallbackReadPreference: 'secondaryPreferred',
}
);
const schemaData = await mongodbSchema(docs);
Expand Down
3 changes: 2 additions & 1 deletion packages/compass-schema/src/stores/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Schema } from 'mongodb-schema';
import type { Action, AnyAction, Reducer } from 'redux';
import type { AggregateOptions } from 'mongodb';
import { type AnalysisState } from '../constants/analysis-states';
import {
ANALYSIS_STATE_ANALYZING,
Expand Down Expand Up @@ -250,7 +251,7 @@ export const startAnalysis = (): SchemaThunkAction<
fields: query.project ?? undefined,
};

const driverOptions = {
const driverOptions: AggregateOptions = {
maxTimeMS: capMaxTimeMSAtPreferenceLimit(preferences, query.maxTimeMS),
};

Expand Down
125 changes: 86 additions & 39 deletions packages/compass-schema/src/stores/store.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { activateSchemaPlugin, type SchemaStore } from './store';
import { activateSchemaPlugin } from './store';
import type { SchemaStore, SchemaPluginServices } from './store';
import AppRegistry, { createActivateHelpers } from 'hadron-app-registry';
import { expect } from 'chai';

Expand All @@ -7,7 +8,6 @@ import { createSandboxFromDefaultPreferences } from 'compass-preferences-model';
import { createNoopLogger } from '@mongodb-js/compass-logging/provider';
import type { FieldStoreService } from '@mongodb-js/compass-field-store';
import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider';
import type { ConnectionInfoRef } from '@mongodb-js/compass-connections/provider';
import { startAnalysis, stopAnalysis } from './reducer';
import Sinon from 'sinon';

Expand All @@ -26,46 +26,61 @@ const mockQueryBar = {
};

describe('Schema Store', function () {
describe('#configureStore', function () {
let store: SchemaStore;
let deactivate: () => void;
let sandbox: Sinon.SinonSandbox;
const localAppRegistry = new AppRegistry();
const globalAppRegistry = new AppRegistry();
const namespace = 'db.coll';
let store: SchemaStore;
let deactivate: () => void;
let sandbox: Sinon.SinonSandbox;
const localAppRegistry = new AppRegistry();
const globalAppRegistry = new AppRegistry();
const namespace = 'db.coll';
let sampleStub: Sinon.SinonStub;
let isCancelErrorStub: Sinon.SinonStub;

beforeEach(function () {
sandbox = Sinon.createSandbox();
sampleStub = sandbox.stub();
isCancelErrorStub = sandbox.stub();
});

async function createStore(services: Partial<SchemaPluginServices> = {}) {
const dataService = {
sample: sampleStub,
isCancelError: isCancelErrorStub,
};
const connectionInfoRef = {
current: {},
} as ConnectionInfoRef;
let sampleStub: Sinon.SinonStub;
let isCancelErrorStub: Sinon.SinonStub;
current: {
connectionOptions: {
connectionString: 'mongodb://localhost:27017',
},
title: 'test',
id: 'test',
},
};

const plugin = activateSchemaPlugin(
{
namespace: namespace,
},
{
localAppRegistry: localAppRegistry,
globalAppRegistry: globalAppRegistry,
dataService,
logger: dummyLogger,
track: dummyTrack,
preferences: await createSandboxFromDefaultPreferences(),
fieldStoreService: mockFieldStoreService,
queryBar: mockQueryBar as any,
connectionInfoRef,
...services,
},
createActivateHelpers()
);
store = plugin.store;
deactivate = () => plugin.deactivate();
}

describe('#configureStore', function () {
beforeEach(async function () {
sandbox = Sinon.createSandbox();
sampleStub = sandbox.stub();
isCancelErrorStub = sandbox.stub();
const dataService = {
sample: sampleStub,
isCancelError: isCancelErrorStub,
};
const plugin = activateSchemaPlugin(
{
namespace: namespace,
},
{
localAppRegistry: localAppRegistry,
globalAppRegistry: globalAppRegistry,
dataService: dataService as any,
logger: dummyLogger,
track: dummyTrack,
preferences: await createSandboxFromDefaultPreferences(),
fieldStoreService: mockFieldStoreService,
queryBar: mockQueryBar as any,
connectionInfoRef,
},
createActivateHelpers()
);
store = plugin.store;
deactivate = () => plugin.deactivate();
await createStore();
});

afterEach(function () {
Expand Down Expand Up @@ -107,5 +122,37 @@ describe('Schema Store', function () {
await analysisPromise;
expect(store.getState().analysisState).to.equal('initial');
});

it('runs the analysis with fallback read pref secondaryPreferred', async function () {
sampleStub.resolves([{ name: 'Hans' }, { name: 'Greta' }]);
await store.dispatch(startAnalysis());
expect(sampleStub.getCall(0).args[3])
.property('fallbackReadPreference')
.to.equal('secondaryPreferred');
});
});

describe('with a connection string with explicit read preference set', function () {
beforeEach(async function () {
await createStore({
connectionInfoRef: {
current: {
connectionOptions: {
connectionString:
'mongodb://localhost:27017/?readPreference=primary',
},
title: 'test',
id: 'test',
},
},
});
});

it('does not set read preference to secondaryPreferred', async function () {
await store.dispatch(startAnalysis());
expect(sampleStub.getCall(0).args[2]).not.to.have.property(
'readPreference'
);
});
});
});
49 changes: 49 additions & 0 deletions packages/data-service/src/data-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Collection, MongoServerError } from 'mongodb';
import { MongoClient } from 'mongodb';
import { Int32, UUID } from 'bson';
import sinon from 'sinon';
import ConnectionStringUrl from 'mongodb-connection-string-url';
import type { DataService } from './data-service';
import { DataServiceImpl } from './data-service';
import type {
Expand Down Expand Up @@ -866,6 +867,54 @@ describe('DataService', function () {
{ allowDiskUse: false }
);
});

it('allows to pass fallbackReadPreference and sets the read preference when unset', function () {
sandbox.spy(dataService, 'aggregate');
void dataService.sample(
'db.coll',
{},
{},
{
fallbackReadPreference: 'secondaryPreferred',
}
);

// eslint-disable-next-line @typescript-eslint/unbound-method
expect(dataService.aggregate).to.have.been.calledWith(
'db.coll',
[{ $sample: { size: 1000 } }],
{ allowDiskUse: true, readPreference: 'secondaryPreferred' }
);
});

it('allows to pass fallbackReadPreference and does not set the read preference when it is already set', function () {
sandbox.spy(dataService, 'aggregate');
const connectionStringReplacement = new ConnectionStringUrl(
cluster().connectionString
);
connectionStringReplacement.searchParams.set(
'readPreference',
'primary'
);
sandbox.replace(dataService as any, '_connectionOptions', {
connectionString: connectionStringReplacement.toString(),
});
void dataService.sample(
'db.coll',
{},
{},
{
fallbackReadPreference: 'secondaryPreferred',
}
);

// eslint-disable-next-line @typescript-eslint/unbound-method
expect(dataService.aggregate).to.have.been.calledWith(
'db.coll',
[{ $sample: { size: 1000 } }],
{ allowDiskUse: true }
);
});
});

describe('#getLastSeenTopology', function () {
Expand Down
24 changes: 22 additions & 2 deletions packages/data-service/src/data-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import type {
ClientEncryptionDataKeyProvider,
ClientEncryptionCreateDataKeyProviderOptions,
SearchIndexDescription,
ReadPreferenceMode,
} from 'mongodb';
import ConnectionStringUrl from 'mongodb-connection-string-url';
import parseNamespace from 'mongodb-ns';
Expand Down Expand Up @@ -122,6 +123,12 @@ function isEmptyObject(obj: Record<string, unknown>) {
return Object.keys(obj).length === 0;
}

function isReadPreferenceSet(connectionString: string): boolean {
return !!new ConnectionStringUrl(connectionString).searchParams.get(
'readPreference'
);
}

let id = 0;

type ClientType = 'CRUD' | 'META';
Expand Down Expand Up @@ -661,7 +668,9 @@ export interface DataService {
ns: string,
args?: { query?: Filter<Document>; size?: number; fields?: Document },
options?: AggregateOptions,
executionOptions?: ExecutionOptions
executionOptions?: ExecutionOptions & {
fallbackReadPreference?: ReadPreferenceMode;
}
): Promise<Document[]>;

/*** Insert ***/
Expand Down Expand Up @@ -2182,7 +2191,9 @@ class DataServiceImpl extends WithLogContext implements DataService {
fields,
}: { query?: Filter<Document>; size?: number; fields?: Document } = {},
options: AggregateOptions = {},
executionOptions?: ExecutionOptions
executionOptions?: ExecutionOptions & {
fallbackReadPreference?: ReadPreferenceMode;
}
): Promise<Document[]> {
const pipeline = [];
if (query && Object.keys(query).length > 0) {
Expand All @@ -2209,6 +2220,15 @@ class DataServiceImpl extends WithLogContext implements DataService {
pipeline,
{
allowDiskUse: true,
// When the read preference isn't set in the connection string explicitly,
// then we allow consumers to default to a read preference, for instance
// secondaryPreferred to avoid using the primary for analyzing documents.
...(executionOptions?.fallbackReadPreference &&
!isReadPreferenceSet(this._connectionOptions.connectionString)
? {
readPreference: executionOptions?.fallbackReadPreference,
}
: {}),
...options,
},
executionOptions
Expand Down
Loading