diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/model/ConcurrencyDocument.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/model/ConcurrencyDocument.ts new file mode 100644 index 00000000..6e842382 --- /dev/null +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/model/ConcurrencyDocument.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +import { DocumentUuid } from '@edfi/meadowlark-core'; + +// By having a unique DocumentUuid at a given time, we handle concurrency. +export interface ConcurrencyDocument { + _id: DocumentUuid; +} diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Db.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Db.ts index f301cef0..b2f2df5c 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Db.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Db.ts @@ -3,14 +3,16 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -import { Collection, MongoClient, ReadConcernLevel, W, ClientSession, ObjectId, FindOptions, ReplaceOptions } from 'mongodb'; -import { Logger, Config } from '@edfi//meadowlark-utilities'; -import { MeadowlarkId } from '@edfi/meadowlark-core'; -import { MeadowlarkDocument } from '../model/MeadowlarkDocument'; -import { AuthorizationDocument } from '../model/AuthorizationDocument'; +import { Collection, MongoClient, ReadConcernLevel, W, ClientSession, FindOptions, ReplaceOptions } from 'mongodb'; +import { Logger, Config } from '@edfi/meadowlark-utilities'; +import type { DocumentUuid } from '@edfi/meadowlark-core'; +import type { MeadowlarkDocument } from '../model/MeadowlarkDocument'; +import type { ConcurrencyDocument } from '../model/ConcurrencyDocument'; +import type { AuthorizationDocument } from '../model/AuthorizationDocument'; export const DOCUMENT_COLLECTION_NAME = 'documents'; export const AUTHORIZATION_COLLECTION_NAME = 'authorizations'; +export const CONCURRENCY_COLLECTION_NAME = 'concurrency'; let singletonClient: MongoClient | null = null; @@ -42,6 +44,12 @@ export async function getNewClient(): Promise { .collection(AUTHORIZATION_COLLECTION_NAME); await authorizationCollection.createIndex({ clientName: 1 }); + // Create concurrency collection if not exists. + const concurrencyCollection: Collection = newClient + .db(databaseName) + .collection(CONCURRENCY_COLLECTION_NAME); + await concurrencyCollection.createIndex({ _id: 1 }); + return newClient; } catch (e) { const message = e instanceof Error ? e.message : 'unknown'; @@ -89,23 +97,8 @@ export function getAuthorizationCollection(client: MongoClient): Collection('MEADOWLARK_DATABASE_NAME')).collection(AUTHORIZATION_COLLECTION_NAME); } -/** - * Write lock referenced documents as part of the upsert/update process. This will prevent the issue of - * a concurrent delete operation removing a to-be referenced document in the middle of the transaction. - * See https://www.mongodb.com/blog/post/how-to-select--for-update-inside-mongodb-transactions - * - * This function expects Session to have an active transaction. Aborting the transaction on error is left to the caller. - */ -export async function writeLockReferencedDocuments( - mongoCollection: Collection, - referencedMeadowlarkIds: MeadowlarkId[], - session: ClientSession, -): Promise { - await mongoCollection.updateMany( - { aliasMeadowlarkIds: { $in: referencedMeadowlarkIds } }, - { $set: { lock: new ObjectId() } }, - { session }, - ); +export function getConcurrencyCollection(client: MongoClient): Collection { + return client.db(Config.get('MEADOWLARK_DATABASE_NAME')).collection(CONCURRENCY_COLLECTION_NAME); } // MongoDB FindOption to return only the indexed _id field, making this a covered query (MongoDB will optimize) @@ -134,3 +127,26 @@ export const asUpsert = (session: ClientSession): ReplaceOptions => ({ upsert: t // MongoDB FindOption to return at most 5 documents export const limitFive = (session: ClientSession): FindOptions => ({ limit: 5, session }); + +/** + * Lock in-use meadowlark documents, both those being directly updated and those being referenced. + */ +export async function lockDocuments( + concurrencyCollection: Collection, + concurrencyDocuments: ConcurrencyDocument[], + session: ClientSession, +): Promise { + await concurrencyCollection.insertMany(concurrencyDocuments, { session }); + + // eslint-disable-next-line no-underscore-dangle + const documentUuids: DocumentUuid[] = concurrencyDocuments.map((document) => document._id); + + await concurrencyCollection.deleteMany( + { + _id: { + $in: documentUuids, + }, + }, + { session }, + ); +} diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Delete.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Delete.ts index 404733b3..3d399e99 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Delete.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Delete.ts @@ -10,8 +10,9 @@ import { DeleteResult, DeleteRequest, ReferringDocumentInfo, DocumentUuid, Trace import { ClientSession, Collection, MongoClient, WithId } from 'mongodb'; import retry from 'async-retry'; import { MeadowlarkDocument } from '../model/MeadowlarkDocument'; -import { getDocumentCollection, limitFive, onlyReturnId } from './Db'; +import { getConcurrencyCollection, getDocumentCollection, lockDocuments, limitFive, onlyReturnId } from './Db'; import { onlyReturnAliasIds, onlyDocumentsReferencing } from './ReferenceValidation'; +import { ConcurrencyDocument } from '../model/ConcurrencyDocument'; const moduleName: string = 'mongodb.repository.Delete'; @@ -75,6 +76,7 @@ async function checkForReferencesToDocument( export async function deleteDocumentByMeadowlarkIdTransaction( { documentUuid, validateNoReferencesToDocument, traceId }: DeleteRequest, mongoCollection: Collection, + concurrencyCollection: Collection, session: ClientSession, ): Promise { if (validateNoReferencesToDocument) { @@ -93,6 +95,14 @@ export async function deleteDocumentByMeadowlarkIdTransaction( traceId, ); + const concurrencyDocuments: ConcurrencyDocument[] = [ + { + _id: documentUuid, + }, + ]; + + await lockDocuments(concurrencyCollection, concurrencyDocuments, session); + const { acknowledged, deletedCount } = await mongoCollection.deleteOne({ documentUuid }, { session }); if (!acknowledged) { @@ -119,13 +129,19 @@ export async function deleteDocumentByDocumentUuid( let deleteResult: DeleteResult = { response: 'UNKNOWN_FAILURE', failureMessage: '' }; try { const mongoCollection: Collection = getDocumentCollection(client); + const concurrencyCollection: Collection = getConcurrencyCollection(client); const numberOfRetries: number = Config.get('MONGODB_MAX_NUMBER_OF_RETRIES'); await retry( async () => { await session.withTransaction(async () => { - deleteResult = await deleteDocumentByMeadowlarkIdTransaction(deleteRequest, mongoCollection, session); + deleteResult = await deleteDocumentByMeadowlarkIdTransaction( + deleteRequest, + mongoCollection, + concurrencyCollection, + session, + ); if (deleteResult.response !== 'DELETE_SUCCESS') { await session.abortTransaction(); } @@ -143,20 +159,16 @@ export async function deleteDocumentByDocumentUuid( ); } catch (e) { Logger.error(`${moduleName}.deleteDocumentByDocumentUuid`, deleteRequest.traceId, e); + await session.abortTransaction(); - let response: DeleteResult = { response: 'UNKNOWN_FAILURE', failureMessage: e.message }; - - // If this is a MongoError, it has a codeName if (e.codeName === 'WriteConflict') { - response = { + return { response: 'DELETE_FAILURE_WRITE_CONFLICT', failureMessage: 'Write conflict due to concurrent access to this or related resources', }; } - await session.abortTransaction(); - - return response; + return { response: 'UNKNOWN_FAILURE', failureMessage: e.message }; } finally { await session.endSession(); } diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Update.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Update.ts index 6ff9f87c..bb3ac95d 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Update.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Update.ts @@ -10,10 +10,11 @@ import { Logger, Config } from '@edfi/meadowlark-utilities'; import { Collection, ClientSession, MongoClient, WithId } from 'mongodb'; import retry from 'async-retry'; import { MeadowlarkDocument, meadowlarkDocumentFrom } from '../model/MeadowlarkDocument'; -import { getDocumentCollection, limitFive, onlyReturnTimestamps, writeLockReferencedDocuments } from './Db'; +import { getDocumentCollection, limitFive, onlyReturnTimestamps, lockDocuments, getConcurrencyCollection } from './Db'; import { deleteDocumentByMeadowlarkIdTransaction } from './Delete'; import { onlyDocumentsReferencing, validateReferences } from './ReferenceValidation'; import { upsertDocumentTransaction } from './Upsert'; +import { ConcurrencyDocument } from '../model/ConcurrencyDocument'; const moduleName: string = 'mongodb.repository.Update'; @@ -26,6 +27,7 @@ const moduleName: string = 'mongodb.repository.Update'; async function insertUpdatedDocument( { meadowlarkId, resourceInfo, documentInfo, edfiDoc, traceId, security }: UpdateRequest, mongoCollection: Collection, + concurrencyCollection: Collection, session: ClientSession, document: MeadowlarkDocument, ): Promise { @@ -40,6 +42,7 @@ async function insertUpdatedDocument( security, }, mongoCollection, + concurrencyCollection, session, document, ); @@ -143,10 +146,31 @@ async function updateAllowingIdentityChange( document: MeadowlarkDocument, updateRequest: UpdateRequest, mongoCollection: Collection, + concurrencyCollection: Collection, session: ClientSession, ): Promise { const { documentUuid, resourceInfo, traceId, security } = updateRequest; + const referringDocumentUuids: WithId[] = await mongoCollection + .find( + { + aliasMeadowlarkIds: { + $in: document.outboundRefs, + }, + }, + + { projection: { documentUuid: 1 } }, + ) + .toArray(); + + const concurrencyDocuments: ConcurrencyDocument[] = referringDocumentUuids.map((referringDocumentUuid) => ({ + _id: referringDocumentUuid.documentUuid, + })); + + concurrencyDocuments.push({ _id: updateRequest.documentUuid }); + + await lockDocuments(concurrencyCollection, concurrencyDocuments, session); + // Optimize happy path by trying a replacement update, which will succeed if there is no identity change const tryUpdateByReplacementResult: UpdateResult | null = await tryUpdateByReplacement( document, @@ -156,8 +180,6 @@ async function updateAllowingIdentityChange( ); if (tryUpdateByReplacementResult != null) { - // Ensure referenced documents are not modified in other transactions - await writeLockReferencedDocuments(mongoCollection, document.outboundRefs, session); return tryUpdateByReplacementResult; } @@ -189,6 +211,7 @@ async function updateAllowingIdentityChange( const deleteResult = await deleteDocumentByMeadowlarkIdTransaction( { documentUuid, resourceInfo, security, validateNoReferencesToDocument: true, traceId }, mongoCollection, + concurrencyCollection, session, ); Logger.debug(`${moduleName}.updateAllowingIdentityChange: Updating document uuid ${documentUuid}`, traceId); @@ -196,7 +219,7 @@ async function updateAllowingIdentityChange( switch (deleteResult.response) { case 'DELETE_SUCCESS': // document was deleted, so we can insert the new version - return insertUpdatedDocument(updateRequest, mongoCollection, session, document); + return insertUpdatedDocument(updateRequest, mongoCollection, concurrencyCollection, session, document); case 'DELETE_FAILURE_NOT_EXISTS': // document was not found on delete, which shouldn't happen return { @@ -227,6 +250,7 @@ async function updateDisallowingIdentityChange( document: MeadowlarkDocument, updateRequest: UpdateRequest, mongoCollection: Collection, + concurrencyCollection: Collection, session: ClientSession, ): Promise { // Perform the document update @@ -235,8 +259,26 @@ async function updateDisallowingIdentityChange( updateRequest.traceId, ); - // Ensure referenced documents are not modified in other transactions - await writeLockReferencedDocuments(mongoCollection, document.outboundRefs, session); + const referringDocumentUuids: WithId[] = await mongoCollection + .find( + { + aliasMeadowlarkIds: { + $in: document.outboundRefs, + }, + }, + { projection: { documentUuid: 1 } }, + ) + .toArray(); + + const concurrencyDocuments: ConcurrencyDocument[] = referringDocumentUuids.map((referringDocumentUuid) => ({ + _id: referringDocumentUuid.documentUuid, + })); + + concurrencyDocuments.push({ _id: updateRequest.documentUuid }); + + // Inserting the same DocumentUuid in Concurrency Collection will result in a WriteConflict error + // By generating this conflict we handle concurrency + await lockDocuments(concurrencyCollection, concurrencyDocuments, session); const tryUpdateByReplacementResult: UpdateResult | null = await tryUpdateByReplacement( document, @@ -319,6 +361,7 @@ async function checkForInvalidReferences( async function updateDocumentByDocumentUuidTransaction( updateRequest: UpdateRequest, mongoCollection: Collection, + concurrencyCollection: Collection, session: ClientSession, ): Promise { const { meadowlarkId, documentUuid, resourceInfo, documentInfo, edfiDoc, validateDocumentReferencesExist, security } = @@ -345,9 +388,9 @@ async function updateDocumentByDocumentUuidTransaction( lastModifiedAt: documentInfo.requestTimestamp, }); if (resourceInfo.allowIdentityUpdates) { - return updateAllowingIdentityChange(document, updateRequest, mongoCollection, session); + return updateAllowingIdentityChange(document, updateRequest, mongoCollection, concurrencyCollection, session); } - return updateDisallowingIdentityChange(document, updateRequest, mongoCollection, session); + return updateDisallowingIdentityChange(document, updateRequest, mongoCollection, concurrencyCollection, session); } /** @@ -362,6 +405,7 @@ export async function updateDocumentByDocumentUuid( Logger.info(`${moduleName}.updateDocumentByDocumentUuid ${documentUuid}`, traceId); const mongoCollection: Collection = getDocumentCollection(client); + const concurrencyCollection: Collection = getConcurrencyCollection(client); const session: ClientSession = client.startSession(); let updateResult: UpdateResult = { response: 'UNKNOWN_FAILURE' }; @@ -371,7 +415,12 @@ export async function updateDocumentByDocumentUuid( await retry( async () => { await session.withTransaction(async () => { - updateResult = await updateDocumentByDocumentUuidTransaction(updateRequest, mongoCollection, session); + updateResult = await updateDocumentByDocumentUuidTransaction( + updateRequest, + mongoCollection, + concurrencyCollection, + session, + ); if (updateResult.response !== 'UPDATE_SUCCESS') { await session.abortTransaction(); } @@ -391,7 +440,6 @@ export async function updateDocumentByDocumentUuid( Logger.error(`${moduleName}.updateDocumentByDocumentUuid`, traceId, e); await session.abortTransaction(); - // If this is a MongoError, it has a codeName if (e.codeName === 'WriteConflict') { return { response: 'UPDATE_FAILURE_WRITE_CONFLICT', diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Upsert.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Upsert.ts index 5d4409cc..4023a4e6 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Upsert.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/src/repository/Upsert.ts @@ -19,19 +19,22 @@ import { Logger, Config } from '@edfi/meadowlark-utilities'; import retry from 'async-retry'; import { MeadowlarkDocument, meadowlarkDocumentFrom } from '../model/MeadowlarkDocument'; import { - writeLockReferencedDocuments, asUpsert, limitFive, getDocumentCollection, onlyReturnDocumentUuidAndTimestamps, + lockDocuments, + getConcurrencyCollection, } from './Db'; import { onlyDocumentsReferencing, validateReferences } from './ReferenceValidation'; +import { ConcurrencyDocument } from '../model/ConcurrencyDocument'; const moduleName: string = 'mongodb.repository.Upsert'; export async function upsertDocumentTransaction( { resourceInfo, documentInfo, meadowlarkId, edfiDoc, validateDocumentReferencesExist, traceId, security }: UpsertRequest, mongoCollection: Collection, + concurrencyCollection: Collection, session: ClientSession, documentFromUpdate?: MeadowlarkDocument, ): Promise { @@ -135,7 +138,26 @@ export async function upsertDocumentTransaction( lastModifiedAt: documentInfo.requestTimestamp, }); - await writeLockReferencedDocuments(mongoCollection, document.outboundRefs, session); + const referringDocumentUuids: WithId[] = await mongoCollection + .find( + { + aliasMeadowlarkIds: { + $in: document.outboundRefs, + }, + }, + { projection: { documentUuid: 1 } }, + ) + .toArray(); + + const concurrencyDocuments: ConcurrencyDocument[] = referringDocumentUuids.map((referringDocumentUuid) => ({ + _id: referringDocumentUuid.documentUuid, + })); + concurrencyDocuments.push({ _id: documentUuid }); + + // Inserting the same DocumentUuid in Concurrency Collection will result in a WriteConflict error + // By generating this conflict we handle concurrency + await lockDocuments(concurrencyCollection, concurrencyDocuments, session); + // Perform the document upsert Logger.debug(`${moduleName}.upsertDocumentTransaction Upserting document uuid ${documentUuid}`, traceId); @@ -172,6 +194,7 @@ export async function upsertDocumentTransaction( */ export async function upsertDocument(upsertRequest: UpsertRequest, client: MongoClient): Promise { const mongoCollection: Collection = getDocumentCollection(client); + const concurrencyCollection: Collection = getConcurrencyCollection(client); const session: ClientSession = client.startSession(); let upsertResult: UpsertResult = { response: 'UNKNOWN_FAILURE' }; try { @@ -180,7 +203,7 @@ export async function upsertDocument(upsertRequest: UpsertRequest, client: Mongo await retry( async () => { await session.withTransaction(async () => { - upsertResult = await upsertDocumentTransaction(upsertRequest, mongoCollection, session); + upsertResult = await upsertDocumentTransaction(upsertRequest, mongoCollection, concurrencyCollection, session); if (upsertResult.response !== 'UPDATE_SUCCESS' && upsertResult.response !== 'INSERT_SUCCESS') { await session.abortTransaction(); } @@ -188,7 +211,7 @@ export async function upsertDocument(upsertRequest: UpsertRequest, client: Mongo }, { retries: numberOfRetries, - onRetry: () => { + onRetry: async () => { Logger.warn( `${moduleName}.upsertDocument got write conflict error for meadowlarkId ${upsertRequest.meadowlarkId}. Retrying...`, upsertRequest.traceId, @@ -200,7 +223,6 @@ export async function upsertDocument(upsertRequest: UpsertRequest, client: Mongo Logger.error(`${moduleName}.upsertDocument`, upsertRequest.traceId, e); await session.abortTransaction(); - // If this is a MongoError, it has a codeName if (e.codeName === 'WriteConflict') { return { response: 'UPSERT_FAILURE_WRITE_CONFLICT', diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/Db.test.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/Db.test.ts new file mode 100644 index 00000000..f50c17fc --- /dev/null +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/Db.test.ts @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +import { DocumentUuid } from '@edfi/meadowlark-core'; +import { MongoClient } from 'mongodb'; +import { getConcurrencyCollection, getNewClient, lockDocuments } from '../../src/repository/Db'; +import { setupConfigForIntegration } from './Config'; +import { ConcurrencyDocument } from '../../src/model/ConcurrencyDocument'; + +describe('when lockDocuments is called with a given number of documents', () => { + let client; + let session; + + beforeAll(async () => { + await setupConfigForIntegration(); + client = (await getNewClient()) as MongoClient; + session = client.startSession(); + + const concurrencyDocuments: ConcurrencyDocument[] = [ + { + _id: '123' as DocumentUuid, + }, + { + _id: '456' as DocumentUuid, + }, + ]; + + await lockDocuments(getConcurrencyCollection(client), concurrencyDocuments, session); + }); + + it('concurrencyCollection should be empty after the function is called', async () => { + const documents = await getConcurrencyCollection(client).countDocuments({ + meadowlarkId: { + $in: ['123', '456'], + }, + }); + + expect(documents).toBe(0); + }); + + afterAll(async () => { + session.endSession(); + await client.close(); + }); +}); diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/locking/Delete.test.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/locking/Delete.test.ts index c51f7e37..aac7cf6a 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/locking/Delete.test.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/locking/Delete.test.ts @@ -5,7 +5,6 @@ import { DocumentInfo, - NoDocumentInfo, newDocumentInfo, newSecurity, meadowlarkIdForDocumentIdentity, @@ -15,17 +14,17 @@ import { ResourceInfo, newResourceInfo, DocumentUuid, - MeadowlarkId, TraceId, } from '@edfi/meadowlark-core'; -import { ClientSession, Collection, MongoClient } from 'mongodb'; +import { ClientSession, Collection, MongoClient, WithId } from 'mongodb'; import { MeadowlarkDocument, meadowlarkDocumentFrom } from '../../../src/model/MeadowlarkDocument'; import { asUpsert, + getConcurrencyCollection, getDocumentCollection, getNewClient, + lockDocuments, onlyReturnId, - writeLockReferencedDocuments, } from '../../../src/repository/Db'; import { validateReferences, @@ -34,19 +33,30 @@ import { } from '../../../src/repository/ReferenceValidation'; import { upsertDocument } from '../../../src/repository/Upsert'; import { setupConfigForIntegration } from '../Config'; +import { ConcurrencyDocument } from '../../../src/model/ConcurrencyDocument'; const documentUuid = '2edb604f-eab0-412c-a242-508d6529214d' as DocumentUuid; // A bunch of setup stuff -const newUpsertRequest = (): UpsertRequest => ({ - meadowlarkId: '' as MeadowlarkId, - resourceInfo: NoResourceInfo, - documentInfo: NoDocumentInfo, - edfiDoc: {}, - validateDocumentReferencesExist: false, - security: { ...newSecurity() }, - traceId: 'traceId' as TraceId, -}); +const edfiSchoolDoc = { + schoolId: 123, + nameOfInstitution: 'A School 123', + educationOrganizationCategories: [ + { + educationOrganizationCategoryDescriptor: 'uri://ed-fi.org/EducationOrganizationCategoryDescriptor#Other', + }, + ], + schoolCategories: [ + { + schoolCategoryDescriptor: 'uri://ed-fi.org/SchoolCategoryDescriptor#All Levels', + }, + ], + gradeLevels: [ + { + gradeLevelDescriptor: 'uri://ed-fi.org/GradeLevelDescriptor#First Grade', + }, + ], +}; const schoolResourceInfo: ResourceInfo = { ...newResourceInfo(), @@ -57,8 +67,19 @@ const schoolDocumentInfo: DocumentInfo = { ...newDocumentInfo(), documentIdentity: { schoolId: '123' }, }; + const schoolMeadowlarkId = meadowlarkIdForDocumentIdentity(schoolResourceInfo, schoolDocumentInfo.documentIdentity); +const newUpsertRequest = (): UpsertRequest => ({ + meadowlarkId: schoolMeadowlarkId, + resourceInfo: NoResourceInfo, + documentInfo: schoolDocumentInfo, + edfiDoc: edfiSchoolDoc, + validateDocumentReferencesExist: false, + security: { ...newSecurity() }, + traceId: 'traceId' as TraceId, +}); + const referenceToSchool: DocumentReference = { projectName: schoolResourceInfo.projectName, resourceName: schoolResourceInfo.resourceName, @@ -66,6 +87,18 @@ const referenceToSchool: DocumentReference = { isDescriptor: false, }; +const schoolDocument: MeadowlarkDocument = meadowlarkDocumentFrom({ + resourceInfo: schoolResourceInfo, + documentInfo: schoolDocumentInfo, + documentUuid, + meadowlarkId: schoolMeadowlarkId, + edfiDoc: edfiSchoolDoc, + validate: true, + createdBy: '', + createdAt: Date.now(), + lastModifiedAt: Date.now(), +}); + const academicWeekResourceInfo: ResourceInfo = { ...newResourceInfo(), resourceName: 'AcademicWeek', @@ -103,7 +136,8 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted await setupConfigForIntegration(); client = (await getNewClient()) as MongoClient; - const mongoCollection: Collection = getDocumentCollection(client); + const mongoDocumentCollection: Collection = getDocumentCollection(client); + const mongoConcurrencyCollection: Collection = getConcurrencyCollection(client); // Insert a School document - it will be referenced by an AcademicWeek document while being deleted await upsertDocument( @@ -121,7 +155,7 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted const upsertFailures = await validateReferences( academicWeekDocumentInfo.documentReferences, [], - mongoCollection, + mongoDocumentCollection, upsertSession, '', ); @@ -129,10 +163,6 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted // Should be no reference validation failures for AcademicWeek document expect(upsertFailures).toHaveLength(0); - // ***** Read-for-write lock the validated referenced documents in the insert - // see https://www.mongodb.com/blog/post/how-to-select--for-update-inside-mongodb-transactions - await writeLockReferencedDocuments(mongoCollection, academicWeekDocument.outboundRefs, upsertSession); - // ---- // Start transaction to delete the School document - interferes with the AcademicWeek insert referencing the School // ---- @@ -140,13 +170,13 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted deleteSession.startTransaction(); // Get the aliasMeadowlarkIds for the School document, used to check for references to it as School or as EducationOrganization - const deleteCandidate: any = await mongoCollection.findOne( + const deleteCandidate: any = await mongoDocumentCollection.findOne( { _id: schoolMeadowlarkId }, onlyReturnAliasIds(deleteSession), ); // Check for any references to the School document - const anyReferences = await mongoCollection.findOne( + const anyReferences = await mongoDocumentCollection.findOne( onlyDocumentsReferencing(deleteCandidate.aliasMeadowlarkIds), onlyReturnId(deleteSession), ); @@ -155,7 +185,7 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted expect(anyReferences).toBeNull(); // Perform the insert of AcademicWeek document, adding a reference to to to-be-deleted document - const { upsertedCount } = await mongoCollection.replaceOne( + const { upsertedCount } = await mongoDocumentCollection.replaceOne( { _id: academicWeekMeadowlarkId }, academicWeekDocument, asUpsert(upsertSession), @@ -164,18 +194,38 @@ describe('given a delete concurrent with an insert referencing the to-be-deleted // **** The insert of AcademicWeek document should have been successful expect(upsertedCount).toBe(1); + // Adds the academic week and the, to be updated, school to the concurrency collection. + const concurrencyDocumentsAcademicWeek: ConcurrencyDocument[] = []; + concurrencyDocumentsAcademicWeek.push({ _id: documentUuid }); + + const schoolDocumentUuid: WithId | null = await mongoDocumentCollection.findOne( + { _id: schoolMeadowlarkId }, + { projection: { documentUuid: 1 } }, + ); + if (schoolDocumentUuid) concurrencyDocumentsAcademicWeek.push({ _id: schoolDocumentUuid?.documentUuid }); + + await lockDocuments(mongoConcurrencyCollection, concurrencyDocumentsAcademicWeek, upsertSession); + // ---- // End transaction to insert the AcademicWeek document // ---- await upsertSession.commitTransaction(); + const concurrencyDocumentsSchool: ConcurrencyDocument[] = []; + concurrencyDocumentsSchool.push({ + _id: schoolDocument.documentUuid, + }); + // Try deleting the School document - should fail thanks to AcademicWeek's read-for-write lock try { - await mongoCollection.deleteOne({ _id: schoolMeadowlarkId }, { session: deleteSession }); + await lockDocuments(mongoConcurrencyCollection, concurrencyDocumentsSchool, deleteSession); + + await mongoDocumentCollection.deleteOne({ _id: schoolMeadowlarkId }, { session: deleteSession }); } catch (e) { expect(e).toMatchInlineSnapshot( - `[MongoServerError: WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.]`, + '[MongoBulkWriteError: WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.]', ); + expect(e.code).toBe(112); } // ---- diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/locking/Update.test.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/locking/Update.test.ts new file mode 100644 index 00000000..40a7f1d0 --- /dev/null +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/integration/locking/Update.test.ts @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +import { + DocumentInfo, + newDocumentInfo, + newSecurity, + meadowlarkIdForDocumentIdentity, + DocumentReference, + UpsertRequest, + NoResourceInfo, + ResourceInfo, + newResourceInfo, + DocumentUuid, + TraceId, +} from '@edfi/meadowlark-core'; +import { ClientSession, Collection, MongoClient, WithId } from 'mongodb'; +import { MeadowlarkDocument, meadowlarkDocumentFrom } from '../../../src/model/MeadowlarkDocument'; +import { + asUpsert, + getConcurrencyCollection, + getDocumentCollection, + getNewClient, + onlyReturnId, + lockDocuments, +} from '../../../src/repository/Db'; +import { + validateReferences, + onlyReturnAliasIds, + onlyDocumentsReferencing, +} from '../../../src/repository/ReferenceValidation'; +import { upsertDocument } from '../../../src/repository/Upsert'; +import { setupConfigForIntegration } from '../Config'; +import { ConcurrencyDocument } from '../../../src/model/ConcurrencyDocument'; + +const documentUuid = '2edb604f-eab0-412c-a242-508d6529214d' as DocumentUuid; + +// A bunch of setup stuff +const edfiSchoolDoc = { + schoolId: 123, + nameOfInstitution: 'A School 123', + educationOrganizationCategories: [ + { + educationOrganizationCategoryDescriptor: 'uri://ed-fi.org/EducationOrganizationCategoryDescriptor#Other', + }, + ], + schoolCategories: [ + { + schoolCategoryDescriptor: 'uri://ed-fi.org/SchoolCategoryDescriptor#All Levels', + }, + ], + gradeLevels: [ + { + gradeLevelDescriptor: 'uri://ed-fi.org/GradeLevelDescriptor#First Grade', + }, + ], +}; + +const schoolResourceInfo: ResourceInfo = { + ...newResourceInfo(), + resourceName: 'School', +}; + +const schoolDocumentInfo: DocumentInfo = { + ...newDocumentInfo(), + documentIdentity: { schoolId: '123' }, +}; + +const schoolMeadowlarkId = meadowlarkIdForDocumentIdentity(schoolResourceInfo, schoolDocumentInfo.documentIdentity); + +const newUpsertRequest = (): UpsertRequest => ({ + meadowlarkId: schoolMeadowlarkId, + resourceInfo: NoResourceInfo, + documentInfo: schoolDocumentInfo, + edfiDoc: edfiSchoolDoc, + validateDocumentReferencesExist: false, + security: { ...newSecurity() }, + traceId: 'traceId' as TraceId, +}); + +const referenceToSchool: DocumentReference = { + projectName: schoolResourceInfo.projectName, + resourceName: schoolResourceInfo.resourceName, + documentIdentity: schoolDocumentInfo.documentIdentity, + isDescriptor: false, +}; + +const schoolDocument: MeadowlarkDocument = meadowlarkDocumentFrom({ + resourceInfo: schoolResourceInfo, + documentInfo: schoolDocumentInfo, + documentUuid, + meadowlarkId: schoolMeadowlarkId, + edfiDoc: edfiSchoolDoc, + validate: true, + createdBy: '', + createdAt: Date.now(), + lastModifiedAt: Date.now(), +}); + +const academicWeekResourceInfo: ResourceInfo = { + ...newResourceInfo(), + resourceName: 'AcademicWeek', +}; +const academicWeekDocumentInfo: DocumentInfo = { + ...newDocumentInfo(), + documentIdentity: { + schoolId: '123', + weekIdentifier: '123456', + }, + + documentReferences: [referenceToSchool], +}; +const academicWeekMeadowlarkId = meadowlarkIdForDocumentIdentity( + academicWeekResourceInfo, + academicWeekDocumentInfo.documentIdentity, +); + +const academicWeekDocument: MeadowlarkDocument = meadowlarkDocumentFrom({ + resourceInfo: academicWeekResourceInfo, + documentInfo: academicWeekDocumentInfo, + documentUuid, + meadowlarkId: academicWeekMeadowlarkId, + edfiDoc: {}, + validate: true, + createdBy: '', + createdAt: Date.now(), + lastModifiedAt: Date.now(), +}); + +describe('given an upsert (update) concurrent with an insert referencing the to-be-updated document - using materialized conflict approach', () => { + let client: MongoClient; + + beforeAll(async () => { + await setupConfigForIntegration(); + + client = (await getNewClient()) as MongoClient; + const mongoDocumentCollection: Collection = getDocumentCollection(client); + const mongoConcurrencyCollection: Collection = getConcurrencyCollection(client); + + // Insert a School document - it will be referenced by an AcademicWeek document while being deleted + await upsertDocument( + { ...newUpsertRequest(), meadowlarkId: schoolMeadowlarkId, documentInfo: schoolDocumentInfo }, + client, + ); + + // ---- + // Start transaction to insert an AcademicWeek - it references the School which will interfere with the School update + // ---- + const upsertSession: ClientSession = client.startSession(); + upsertSession.startTransaction(); + + // Check for reference validation failures on AcademicWeek document - School is still there + const upsertFailures = await validateReferences( + academicWeekDocumentInfo.documentReferences, + [], + mongoDocumentCollection, + upsertSession, + '', + ); + + // Should be no reference validation failures for AcademicWeek document + expect(upsertFailures).toHaveLength(0); + + // ---- + // Start transaction to update the School document - interferes with the AcademicWeek insert referencing the School + // ---- + const updateSession: ClientSession = client.startSession(); + updateSession.startTransaction(); + + // Get the aliasMeadowlarkIds for the School document, used to check for references to it as School or as EducationOrganization + const udpateCandidate: any = await mongoDocumentCollection.findOne( + { _id: schoolMeadowlarkId }, + onlyReturnAliasIds(updateSession), + ); + + // Check for any references to the School document + const anyReferences = await mongoDocumentCollection.findOne( + onlyDocumentsReferencing(udpateCandidate.aliasMeadowlarkIds), + onlyReturnId(updateSession), + ); + + // Update transaction sees no references yet, though we are about to add one + expect(anyReferences).toBeNull(); + + // Perform the insert of AcademicWeek document, adding a reference to to to-be-updated document + const { upsertedCount } = await mongoDocumentCollection.replaceOne( + { _id: academicWeekMeadowlarkId }, + academicWeekDocument, + asUpsert(upsertSession), + ); + + // **** The insert of AcademicWeek document should have been successful + expect(upsertedCount).toBe(1); + + // Adds the academic week and the, to be updated, school to the concurrency collection. + const concurrencyDocumentsAcademicWeek: ConcurrencyDocument[] = []; + concurrencyDocumentsAcademicWeek.push({ _id: documentUuid }); + + const schoolDocumentUuid: WithId | null = await mongoDocumentCollection.findOne( + { _id: schoolMeadowlarkId }, + { projection: { documentUuid: 1 } }, + ); + if (schoolDocumentUuid) concurrencyDocumentsAcademicWeek.push({ _id: schoolDocumentUuid?.documentUuid }); + + await lockDocuments(mongoConcurrencyCollection, concurrencyDocumentsAcademicWeek, upsertSession); + + // ---- + // End transaction to insert the AcademicWeek document + // ---- + await upsertSession.commitTransaction(); + + const concurrencyDocumentsSchool: ConcurrencyDocument[] = []; + concurrencyDocumentsSchool.push({ + _id: schoolDocument.documentUuid, + }); + + // Try updating the School document - should fail thanks to the conflict in concurrency collection + try { + await lockDocuments(mongoConcurrencyCollection, concurrencyDocumentsSchool, updateSession); + + schoolDocument.edfiDoc.nameOfInstitution = 'A School 124'; + + await mongoDocumentCollection.replaceOne({ _id: schoolMeadowlarkId }, schoolDocument, asUpsert(updateSession)); + } catch (e) { + expect(e).toMatchInlineSnapshot( + '[MongoBulkWriteError: WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.]', + ); + expect(e.code).toBe(112); + } + + // ---- + // End transaction to update the School document + // ---- + await updateSession.abortTransaction(); + }); + + it('should still have the initial nameOfInstitution: A School 123', async () => { + const collection: Collection = getDocumentCollection(client); + const result: any = await collection.findOne({ _id: schoolMeadowlarkId }); + expect(result.documentIdentity.schoolId).toBe('123'); + expect(result.edfiDoc.nameOfInstitution).toBe('A School 123'); + }); + + afterAll(async () => { + await getDocumentCollection(client).deleteMany({}); + await getConcurrencyCollection(client).deleteMany({}); + await client.close(); + }); +}); diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Delete.test.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Delete.test.ts index ce7bd9b1..06e29957 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Delete.test.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Delete.test.ts @@ -8,6 +8,7 @@ describe('given a transaction on a resource', () => { let mongoClientMock = {}; let deleteOneMock = jest.fn(); const error = { + code: 112, codeName: 'WriteConflict', }; @@ -18,6 +19,11 @@ describe('given a transaction on a resource', () => { deleteOne: deleteOneMock, } as any); + jest.spyOn(DB, 'getConcurrencyCollection').mockReturnValue({ + insertMany: jest.fn(), + deleteMany: jest.fn(), + } as any); + mongoClientMock = { startSession: jest.fn().mockReturnValue({ withTransaction: async (cb: any) => { diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Update.test.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Update.test.ts index cb9fdf5d..906390cc 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Update.test.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Update.test.ts @@ -16,6 +16,7 @@ describe('given a transaction on a resource', () => { let mongoClientMock = {}; let updateOneMock = jest.fn(); const error = { + code: 112, codeName: 'WriteConflict', }; @@ -26,6 +27,12 @@ describe('given a transaction on a resource', () => { updateMany: jest.fn(), findOne: jest.fn(), updateOne: updateOneMock, + find: jest.fn(() => ({ toArray: (_) => [{ some: 'content' }] })), + } as any); + + jest.spyOn(DB, 'getConcurrencyCollection').mockReturnValue({ + insertMany: jest.fn(), + deleteMany: jest.fn(), } as any); mongoClientMock = { @@ -54,7 +61,6 @@ describe('given a transaction on a resource', () => { describe('given that a number of retries greater than zero has been configured', () => { beforeAll(async () => { - jest.spyOn(DB, 'writeLockReferencedDocuments').mockImplementationOnce(async () => Promise.resolve()); jest.spyOn(utilities.Config, 'get').mockReturnValue(retryNumberOfTimes); result = await updateDocumentByDocumentUuid(newUpdateRequest(), mongoClientMock as any); }); @@ -74,7 +80,6 @@ describe('given a transaction on a resource', () => { describe('given that a number of retries equal to zero has been configured', () => { beforeAll(async () => { - jest.spyOn(DB, 'writeLockReferencedDocuments').mockImplementationOnce(async () => Promise.resolve()); jest.spyOn(utilities.Config, 'get').mockReturnValue(0); result = await updateDocumentByDocumentUuid(newUpdateRequest(), mongoClientMock as any); }); @@ -90,7 +95,6 @@ describe('given a transaction on a resource', () => { describe('given that a number of retries was not configured', () => { beforeAll(async () => { - jest.spyOn(DB, 'writeLockReferencedDocuments').mockImplementationOnce(async () => Promise.resolve()); result = await updateDocumentByDocumentUuid(newUpdateRequest(), mongoClientMock as any); }); diff --git a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Upsert.test.ts b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Upsert.test.ts index eae09e0c..129fac40 100644 --- a/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Upsert.test.ts +++ b/Meadowlark-js/backends/meadowlark-mongodb-backend/test/repository/Upsert.test.ts @@ -8,6 +8,7 @@ describe('given a transaction on a resource', () => { let mongoClientMock = {}; let replaceOneMock = jest.fn(); const error = { + code: 112, codeName: 'WriteConflict', }; @@ -18,6 +19,12 @@ describe('given a transaction on a resource', () => { replaceOne: replaceOneMock, findOne: jest.fn().mockReturnValue(null), updateMany: jest.fn(), + find: jest.fn(() => ({ toArray: (_) => [{ some: 'content' }] })), + } as any); + + jest.spyOn(DB, 'getConcurrencyCollection').mockReturnValue({ + insertMany: jest.fn(), + deleteMany: jest.fn(), } as any); mongoClientMock = { @@ -45,7 +52,6 @@ describe('given a transaction on a resource', () => { describe('given that a number of retries greater than zero has been configured', () => { beforeAll(async () => { - jest.spyOn(DB, 'writeLockReferencedDocuments').mockImplementationOnce(async () => Promise.resolve()); jest.spyOn(utilities.Config, 'get').mockReturnValue(retryNumberOfTimes); result = await upsertDocument(newUpsertRequest(), mongoClientMock as any); }); @@ -65,7 +71,6 @@ describe('given a transaction on a resource', () => { describe('given that a number of retries equal to zero has been configured', () => { beforeAll(async () => { - jest.spyOn(DB, 'writeLockReferencedDocuments').mockImplementationOnce(async () => Promise.resolve()); jest.spyOn(utilities.Config, 'get').mockReturnValue(0); result = await upsertDocument(newUpsertRequest(), mongoClientMock as any); }); @@ -81,7 +86,6 @@ describe('given a transaction on a resource', () => { describe('given that a number of retries was not configured', () => { beforeAll(async () => { - jest.spyOn(DB, 'writeLockReferencedDocuments').mockImplementationOnce(async () => Promise.resolve()); result = await upsertDocument(newUpsertRequest(), mongoClientMock as any); }); diff --git a/Meadowlark-js/tests/http/RND-637.http b/Meadowlark-js/tests/http/RND-637.http index 943d9ab8..1d513948 100644 --- a/Meadowlark-js/tests/http/RND-637.http +++ b/Meadowlark-js/tests/http/RND-637.http @@ -64,13 +64,14 @@ authorization: bearer {{authToken1}} ### This returns 409 error, since uri://ed-fi.org/SchoolCategoryDescriptor#All Levels # and uri://ed-fi.org/GradeLevelDescriptor#First Grade don't exist. +# @name theSchool POST http://localhost:3000/local/v3.3b/ed-fi/schools content-type: application/json authorization: bearer {{authToken1}} { - "schoolId": 124, - "nameOfInstitution": "A School", + "schoolId": 125, + "nameOfInstitution": "A School 127", "educationOrganizationCategories" : [ { "educationOrganizationCategoryDescriptor": "uri://ed-fi.org/EducationOrganizationCategoryDescriptor#Other" @@ -115,3 +116,30 @@ authorization: bearer {{authToken1}} ##### GET http://localhost:3000/local/v3.3b/ed-fi/schools?nameOfInstitution=A School authorization: bearer {{authToken1}} + +### This returns 409 error, since uri://ed-fi.org/SchoolCategoryDescriptor#All Levels +# and uri://ed-fi.org/GradeLevelDescriptor#First Grade don't exist. +PUT http://localhost:3000{{theSchool.response.headers.location}} +content-type: application/json +authorization: bearer {{authToken1}} + +{ + "id": "28daa358-6154-4973-aad3-2f4073c6544f", + "schoolId": 125, + "nameOfInstitution": "A School 127", + "educationOrganizationCategories" : [ + { + "educationOrganizationCategoryDescriptor": "uri://ed-fi.org/EducationOrganizationCategoryDescriptor#Other" + } + ], + "schoolCategories": [ + { + "schoolCategoryDescriptor": "uri://ed-fi.org/SchoolCategoryDescriptor#All Levels" + } + ], + "gradeLevels": [ + { + "gradeLevelDescriptor": "uri://ed-fi.org/GradeLevelDescriptor#First Grade" + } + ] +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/fifth.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/fifth.txt new file mode 100644 index 00000000..32360e50 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/fifth.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.13, + samples: 160, + start: 2023-10-18T20:33:37.962Z, + finish: 2023-10-18T20:36:18.095Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 11245, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 11244 }, '201': { count: 1 } }, + latency: { + average: 13.72, + mean: 13.72, + stddev: 2.68, + min: 11, + max: 125, + p0_001: 11, + p0_01: 11, + p0_1: 11, + p1: 11, + p2_5: 11, + p10: 12, + p25: 13, + p50: 13, + p75: 14, + p90: 15, + p97_5: 19, + p99: 24, + p99_9: 40, + p99_99: 76, + p99_999: 125, + totalCount: 11245 + }, + requests: { + average: 70.29, + mean: 70.29, + stddev: 5.83, + min: 31, + max: 77, + total: 11245, + p0_001: 31, + p0_01: 31, + p0_1: 31, + p1: 33, + p2_5: 55, + p10: 66, + p25: 69, + p50: 71, + p75: 73, + p90: 74, + p97_5: 75, + p99: 77, + p99_9: 77, + p99_99: 77, + p99_999: 77, + sent: 11246 + }, + throughput: { + average: 24528.43, + mean: 24528.43, + stddev: 2031.97, + min: 10819, + max: 26873, + total: 3924510, + p0_001: 10823, + p0_01: 10823, + p0_1: 10823, + p1: 11519, + p2_5: 19199, + p10: 23039, + p25: 24095, + p50: 24783, + p75: 25487, + p90: 25839, + p97_5: 26175, + p99: 26879, + p99_9: 26879, + p99_99: 26879, + p99_999: 26879 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/first.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/first.txt new file mode 100644 index 00000000..1ea5f48b --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/first.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.1, + samples: 160, + start: 2023-10-18T20:00:55.903Z, + finish: 2023-10-18T20:03:36.001Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 11011, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 11010 }, '201': { count: 1 } }, + latency: { + average: 14.02, + mean: 14.02, + stddev: 2.83, + min: 11, + max: 234, + p0_001: 11, + p0_01: 11, + p0_1: 11, + p1: 12, + p2_5: 12, + p10: 12, + p25: 13, + p50: 14, + p75: 14, + p90: 16, + p97_5: 18, + p99: 20, + p99_9: 32, + p99_99: 46, + p99_999: 234, + totalCount: 11011 + }, + requests: { + average: 68.82, + mean: 68.82, + stddev: 4.95, + min: 39, + max: 74, + total: 11011, + p0_001: 39, + p0_01: 39, + p0_1: 39, + p1: 54, + p2_5: 55, + p10: 61, + p25: 68, + p50: 70, + p75: 72, + p90: 73, + p97_5: 73, + p99: 74, + p99_9: 74, + p99_99: 74, + p99_999: 74, + sent: 11012 + }, + throughput: { + average: 24017.88, + mean: 24017.88, + stddev: 1725.81, + min: 13616, + max: 25826, + total: 3842844, + p0_001: 13623, + p0_01: 13623, + p0_1: 13623, + p1: 18847, + p2_5: 19199, + p10: 21295, + p25: 23743, + p50: 24431, + p75: 25135, + p90: 25487, + p97_5: 25487, + p99: 25839, + p99_9: 25839, + p99_99: 25839, + p99_999: 25839 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/fourth.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/fourth.txt new file mode 100644 index 00000000..a61dc541 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/fourth.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.13, + samples: 160, + start: 2023-10-18T20:28:50.544Z, + finish: 2023-10-18T20:31:30.671Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 11427, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 11426 }, '201': { count: 1 } }, + latency: { + average: 13.49, + mean: 13.49, + stddev: 1.55, + min: 10, + max: 43, + p0_001: 10, + p0_01: 11, + p0_1: 11, + p1: 11, + p2_5: 12, + p10: 12, + p25: 13, + p50: 13, + p75: 14, + p90: 15, + p97_5: 16, + p99: 19, + p99_9: 28, + p99_99: 39, + p99_999: 43, + totalCount: 11427 + }, + requests: { + average: 71.42, + mean: 71.42, + stddev: 2.28, + min: 65, + max: 76, + total: 11427, + p0_001: 65, + p0_01: 65, + p0_1: 65, + p1: 65, + p2_5: 66, + p10: 68, + p25: 70, + p50: 72, + p75: 73, + p90: 74, + p97_5: 75, + p99: 76, + p99_9: 76, + p99_99: 76, + p99_999: 76, + sent: 11428 + }, + throughput: { + average: 24925.5, + mean: 24925.5, + stddev: 795.35, + min: 22685, + max: 26524, + total: 3988028, + p0_001: 22687, + p0_01: 22687, + p0_1: 22687, + p1: 22703, + p2_5: 23039, + p10: 23743, + p25: 24431, + p50: 25135, + p75: 25487, + p90: 25839, + p97_5: 26175, + p99: 26527, + p99_9: 26527, + p99_99: 26527, + p99_999: 26527 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/second.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/second.txt new file mode 100644 index 00000000..e7045489 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/second.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.11, + samples: 160, + start: 2023-10-18T20:19:18.680Z, + finish: 2023-10-18T20:21:58.790Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 11366, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 11365 }, '201': { count: 1 } }, + latency: { + average: 13.56, + mean: 13.56, + stddev: 1.71, + min: 10, + max: 36, + p0_001: 10, + p0_01: 10, + p0_1: 11, + p1: 11, + p2_5: 12, + p10: 12, + p25: 13, + p50: 13, + p75: 14, + p90: 15, + p97_5: 17, + p99: 20, + p99_9: 31, + p99_99: 35, + p99_999: 36, + totalCount: 11366 + }, + requests: { + average: 71.04, + mean: 71.04, + stddev: 2.82, + min: 60, + max: 77, + total: 11366, + p0_001: 60, + p0_01: 60, + p0_1: 60, + p1: 61, + p2_5: 63, + p10: 67, + p25: 70, + p50: 71, + p75: 73, + p90: 74, + p97_5: 75, + p99: 77, + p99_9: 77, + p99_99: 77, + p99_999: 77, + sent: 11367 + }, + throughput: { + average: 24792, + mean: 24792, + stddev: 983.58, + min: 20945, + max: 26873, + total: 3966739, + p0_001: 20959, + p0_01: 20959, + p0_1: 20959, + p1: 21295, + p2_5: 21999, + p10: 23391, + p25: 24431, + p50: 24783, + p75: 25487, + p90: 25839, + p97_5: 26175, + p99: 26879, + p99_9: 26879, + p99_99: 26879, + p99_999: 26879 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/third.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/third.txt new file mode 100644 index 00000000..fbadccb6 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/oneConnection/third.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.1, + samples: 160, + start: 2023-10-18T20:24:09.887Z, + finish: 2023-10-18T20:26:49.984Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 11661, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 11660 }, '201': { count: 1 } }, + latency: { + average: 13.21, + mean: 13.21, + stddev: 1.61, + min: 10, + max: 39, + p0_001: 10, + p0_01: 10, + p0_1: 11, + p1: 11, + p2_5: 11, + p10: 12, + p25: 12, + p50: 13, + p75: 14, + p90: 14, + p97_5: 16, + p99: 19, + p99_9: 31, + p99_99: 36, + p99_999: 39, + totalCount: 11661 + }, + requests: { + average: 72.89, + mean: 72.89, + stddev: 2.11, + min: 66, + max: 77, + total: 11661, + p0_001: 66, + p0_01: 66, + p0_1: 66, + p1: 67, + p2_5: 68, + p10: 70, + p25: 72, + p50: 73, + p75: 74, + p90: 75, + p97_5: 77, + p99: 77, + p99_9: 77, + p99_99: 77, + p99_999: 77, + sent: 11662 + }, + throughput: { + average: 25436.1, + mean: 25436.1, + stddev: 733.86, + min: 23034, + max: 26873, + total: 4069694, + p0_001: 23039, + p0_01: 23039, + p0_1: 23039, + p1: 23391, + p2_5: 23743, + p10: 24431, + p25: 25135, + p50: 25487, + p75: 25839, + p90: 26175, + p97_5: 26879, + p99: 26879, + p99_9: 26879, + p99_99: 26879, + p99_999: 26879 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/fifth.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/fifth.txt new file mode 100644 index 00000000..3bba4a0a --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/fifth.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.15, + samples: 160, + start: 2023-10-18T19:54:13.947Z, + finish: 2023-10-18T19:56:54.100Z, + errors: 42, + timeouts: 42, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 2686, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 2668 }, '201': { count: 18 } }, + latency: { + average: 1320.82, + mean: 1320.82, + stddev: 1453.29, + min: 115, + max: 10000, + p0_001: 115, + p0_01: 115, + p0_1: 127, + p1: 135, + p2_5: 137, + p10: 147, + p25: 237, + p50: 747, + p75: 1942, + p90: 3246, + p97_5: 5127, + p99: 6836, + p99_9: 9892, + p99_99: 10000, + p99_999: 10000, + totalCount: 2686 + }, + requests: { + average: 16.79, + mean: 16.79, + stddev: 3.91, + min: 9, + max: 22, + total: 2686, + p0_001: 9, + p0_01: 9, + p0_1: 9, + p1: 9, + p2_5: 11, + p10: 11, + p25: 13, + p50: 16, + p75: 21, + p90: 21, + p97_5: 22, + p99: 22, + p99_9: 22, + p99_99: 22, + p99_999: 22, + sent: 2753 + }, + throughput: { + average: 5860.2, + mean: 5860.2, + stddev: 1364.22, + min: 3141, + max: 7678, + total: 937504, + p0_001: 3141, + p0_01: 3141, + p0_1: 3141, + p1: 3141, + p2_5: 3839, + p10: 3839, + p25: 4539, + p50: 5587, + p75: 7331, + p90: 7331, + p97_5: 7679, + p99: 7679, + p99_9: 7679, + p99_99: 7679, + p99_999: 7679 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/first.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/first.txt new file mode 100644 index 00000000..a0453da9 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/first.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.12, + samples: 160, + start: 2023-10-18T19:35:13.963Z, + finish: 2023-10-18T19:37:54.083Z, + errors: 19, + timeouts: 19, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 3035, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 3015 }, '201': { count: 20 } }, + latency: { + average: 1246.35, + mean: 1246.35, + stddev: 1391.15, + min: 92, + max: 9930, + p0_001: 92, + p0_01: 92, + p0_1: 130, + p1: 134, + p2_5: 136, + p10: 143, + p25: 202, + p50: 769, + p75: 1776, + p90: 2978, + p97_5: 5049, + p99: 6671, + p99_9: 8987, + p99_99: 9930, + p99_999: 9930, + totalCount: 3035 + }, + requests: { + average: 18.97, + mean: 18.97, + stddev: 2.45, + min: 11, + max: 23, + total: 3035, + p0_001: 11, + p0_01: 11, + p0_1: 11, + p1: 14, + p2_5: 14, + p10: 16, + p25: 17, + p50: 20, + p75: 21, + p90: 22, + p97_5: 22, + p99: 22, + p99_9: 23, + p99_99: 23, + p99_999: 23, + sent: 3079 + }, + throughput: { + average: 6621.62, + mean: 6621.62, + stddev: 851.95, + min: 3869, + max: 8027, + total: 1059315, + p0_001: 3869, + p0_01: 3869, + p0_1: 3869, + p1: 4887, + p2_5: 4887, + p10: 5587, + p25: 5935, + p50: 6983, + p75: 7331, + p90: 7679, + p97_5: 7679, + p99: 7679, + p99_9: 8027, + p99_99: 8027, + p99_999: 8027 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/fourth.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/fourth.txt new file mode 100644 index 00000000..fdd02cf6 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/fourth.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.12, + samples: 160, + start: 2023-10-18T19:49:56.837Z, + finish: 2023-10-18T19:52:36.961Z, + errors: 36, + timeouts: 36, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 2640, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 2620 }, '201': { count: 20 } }, + latency: { + average: 1369, + mean: 1369, + stddev: 1443.87, + min: 127, + max: 9149, + p0_001: 127, + p0_01: 127, + p0_1: 129, + p1: 135, + p2_5: 137, + p10: 148, + p25: 238, + p50: 803, + p75: 2103, + p90: 3416, + p97_5: 5133, + p99: 6338, + p99_9: 8709, + p99_99: 9149, + p99_999: 9149, + totalCount: 2640 + }, + requests: { + average: 16.5, + mean: 16.5, + stddev: 3.93, + min: 10, + max: 22, + total: 2640, + p0_001: 10, + p0_01: 10, + p0_1: 10, + p1: 10, + p2_5: 11, + p10: 11, + p25: 13, + p50: 16, + p75: 20, + p90: 21, + p97_5: 22, + p99: 22, + p99_9: 22, + p99_99: 22, + p99_999: 22, + sent: 2701 + }, + throughput: { + average: 5760.03, + mean: 5760.03, + stddev: 1370.16, + min: 3490, + max: 7678, + total: 921460, + p0_001: 3491, + p0_01: 3491, + p0_1: 3491, + p1: 3491, + p2_5: 3839, + p10: 3839, + p25: 4539, + p50: 5587, + p75: 6983, + p90: 7331, + p97_5: 7679, + p99: 7679, + p99_9: 7679, + p99_99: 7679, + p99_999: 7679 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/second.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/second.txt new file mode 100644 index 00000000..2c2b46a0 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/second.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.12, + samples: 160, + start: 2023-10-18T19:40:53.874Z, + finish: 2023-10-18T19:43:33.997Z, + errors: 47, + timeouts: 47, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 2411, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 2392 }, '201': { count: 19 } }, + latency: { + average: 1450.99, + mean: 1450.99, + stddev: 1688.8, + min: 120, + max: 9751, + p0_001: 120, + p0_01: 120, + p0_1: 129, + p1: 135, + p2_5: 139, + p10: 192, + p25: 246, + p50: 728, + p75: 2056, + p90: 3662, + p97_5: 6348, + p99: 7976, + p99_9: 9358, + p99_99: 9751, + p99_999: 9751, + totalCount: 2411 + }, + requests: { + average: 15.07, + mean: 15.07, + stddev: 3.43, + min: 7, + max: 23, + total: 2411, + p0_001: 7, + p0_01: 7, + p0_1: 7, + p1: 8, + p2_5: 10, + p10: 11, + p25: 13, + p50: 14, + p75: 16, + p90: 21, + p97_5: 22, + p99: 22, + p99_9: 23, + p99_99: 23, + p99_999: 23, + sent: 2483 + }, + throughput: { + average: 5260.19, + mean: 5260.19, + stddev: 1193.95, + min: 2443, + max: 8027, + total: 841534, + p0_001: 2443, + p0_01: 2443, + p0_1: 2443, + p1: 2793, + p2_5: 3491, + p10: 3839, + p25: 4539, + p50: 4887, + p75: 5587, + p90: 7331, + p97_5: 7679, + p99: 7679, + p99_9: 8027, + p99_99: 8027, + p99_999: 8027 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/third.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/third.txt new file mode 100644 index 00000000..9515f697 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/autocannon/twentyFiveConnections/third.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.12, + samples: 160, + start: 2023-10-18T19:45:44.645Z, + finish: 2023-10-18T19:48:24.764Z, + errors: 28, + timeouts: 28, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 2956, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 2937 }, '201': { count: 19 } }, + latency: { + average: 1249.22, + mean: 1249.22, + stddev: 1317.31, + min: 114, + max: 9982, + p0_001: 114, + p0_01: 114, + p0_1: 127, + p1: 134, + p2_5: 137, + p10: 144, + p25: 211, + p50: 787, + p75: 1873, + p90: 3050, + p97_5: 4611, + p99: 5797, + p99_9: 9413, + p99_99: 9982, + p99_999: 9982, + totalCount: 2956 + }, + requests: { + average: 18.48, + mean: 18.48, + stddev: 3.44, + min: 9, + max: 22, + total: 2956, + p0_001: 9, + p0_01: 9, + p0_1: 9, + p1: 11, + p2_5: 12, + p10: 13, + p25: 15, + p50: 20, + p75: 21, + p90: 22, + p97_5: 22, + p99: 22, + p99_9: 22, + p99_99: 22, + p99_999: 22, + sent: 3009 + }, + throughput: { + average: 6449.36, + mean: 6449.36, + stddev: 1197.8, + min: 3141, + max: 7678, + total: 1031739, + p0_001: 3141, + p0_01: 3141, + p0_1: 3141, + p1: 3839, + p2_5: 4191, + p10: 4539, + p25: 5235, + p50: 6983, + p75: 7331, + p90: 7679, + p97_5: 7679, + p99: 7679, + p99_9: 7679, + p99_99: 7679, + p99_999: 7679 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/bulkload/raw-results.txt b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/bulkload/raw-results.txt new file mode 100644 index 00000000..c6f6d63c --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/BaseCode_writeLockReferencedDocuments/bulkload/raw-results.txt @@ -0,0 +1,7 @@ +Bulk load - raw results: + +00:03:54.651 +00:03:52.147 +00:03:52.775 +00:03:54.023 +00:03:55.442 diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/fifth.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/fifth.txt new file mode 100644 index 00000000..f40b325e --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/fifth.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.09, + samples: 160, + start: 2023-10-31T23:04:50.315Z, + finish: 2023-10-31T23:07:30.403Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 10483, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 10482 }, '201': { count: 1 } }, + latency: { + average: 14.74, + mean: 14.74, + stddev: 1.46, + min: 12, + max: 39, + p0_001: 12, + p0_01: 12, + p0_1: 13, + p1: 13, + p2_5: 13, + p10: 13, + p25: 14, + p50: 15, + p75: 15, + p90: 16, + p97_5: 17, + p99: 19, + p99_9: 28, + p99_99: 33, + p99_999: 39, + totalCount: 10483 + }, + requests: { + average: 65.52, + mean: 65.52, + stddev: 1.96, + min: 57, + max: 68, + total: 10483, + p0_001: 57, + p0_01: 57, + p0_1: 57, + p1: 57, + p2_5: 60, + p10: 63, + p25: 65, + p50: 66, + p75: 67, + p90: 68, + p97_5: 68, + p99: 68, + p99_9: 68, + p99_99: 68, + p99_999: 68, + sent: 10484 + }, + throughput: { + average: 22866.5, + mean: 22866.5, + stddev: 681.28, + min: 19893, + max: 23732, + total: 3658572, + p0_001: 19903, + p0_01: 19903, + p0_1: 19903, + p1: 19903, + p2_5: 20943, + p10: 21999, + p25: 22687, + p50: 23039, + p75: 23391, + p90: 23743, + p97_5: 23743, + p99: 23743, + p99_9: 23743, + p99_99: 23743, + p99_999: 23743 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/first.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/first.txt new file mode 100644 index 00000000..e3eb007e --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/first.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.11, + samples: 160, + start: 2023-10-31T22:49:14.809Z, + finish: 2023-10-31T22:51:54.917Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 10119, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 10118 }, '201': { count: 1 } }, + latency: { + average: 14.3, + mean: 14.3, + stddev: 3.22, + min: 13, + max: 284, + p0_001: 13, + p0_01: 13, + p0_1: 13, + p1: 13, + p2_5: 13, + p10: 14, + p25: 14, + p50: 15, + p75: 16, + p90: 17, + p97_5: 19, + p99: 22, + p99_9: 30, + p99_99: 37, + p99_999: 284, + totalCount: 10119 + }, + requests: { + average: 66.25, + mean: 66.25, + stddev: 4.47, + min: 32, + max: 68, + total: 10119, + p0_001: 32, + p0_01: 32, + p0_1: 32, + p1: 51, + p2_5: 52, + p10: 56, + p25: 62, + p50: 64, + p75: 66, + p90: 67, + p97_5: 68, + p99: 68, + p99_9: 68, + p99_99: 68, + p99_999: 68, + sent: 10120 + }, + throughput: { + average: 22072.78, + mean: 22072.78, + stddev: 1559.08, + min: 11173, + max: 23732, + total: 3531536, + p0_001: 11175, + p0_01: 11175, + p0_1: 11175, + p1: 17807, + p2_5: 18159, + p10: 19551, + p25: 21647, + p50: 22351, + p75: 23039, + p90: 23391, + p97_5: 23743, + p99: 23743, + p99_9: 23743, + p99_99: 23743, + p99_999: 23743 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/fourth.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/fourth.txt new file mode 100644 index 00000000..8e74b047 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/fourth.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.1, + samples: 160, + start: 2023-10-31T23:13:45.915Z, + finish: 2023-10-31T23:16:26.017Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 10213, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 10212 }, '201': { count: 1 } }, + latency: { + average: 14.15, + mean: 14.15, + stddev: 1.55, + min: 12, + max: 33, + p0_001: 12, + p0_01: 12, + p0_1: 13, + p1: 13, + p2_5: 13, + p10: 14, + p25: 14, + p50: 15, + p75: 16, + p90: 17, + p97_5: 18, + p99: 20, + p99_9: 28, + p99_99: 33, + p99_999: 33, + totalCount: 10213 + }, + requests: { + average: 63.84, + mean: 63.84, + stddev: 2.43, + min: 58, + max: 68, + total: 10213, + p0_001: 58, + p0_01: 58, + p0_1: 58, + p1: 58, + p2_5: 59, + p10: 60, + p25: 62, + p50: 64, + p75: 65, + p90: 67, + p97_5: 68, + p99: 68, + p99_9: 68, + p99_99: 68, + p99_999: 68, + sent: 10214 + }, + throughput: { + average: 22278.1, + mean: 22278.1, + stddev: 848.27, + min: 20242, + max: 23732, + total: 3564342, + p0_001: 20255, + p0_01: 20255, + p0_1: 20255, + p1: 20255, + p2_5: 20591, + p10: 20943, + p25: 21647, + p50: 22351, + p75: 22687, + p90: 23391, + p97_5: 23743, + p99: 23743, + p99_9: 23743, + p99_99: 23743, + p99_999: 23743 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/second.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/second.txt new file mode 100644 index 00000000..f77ba03c --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/second.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.1, + samples: 160, + start: 2023-10-31T22:56:11.415Z, + finish: 2023-10-31T22:58:51.518Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 10526, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 10525 }, '201': { count: 1 } }, + latency: { + average: 14.69, + mean: 14.69, + stddev: 1.57, + min: 12, + max: 40, + p0_001: 12, + p0_01: 12, + p0_1: 13, + p1: 13, + p2_5: 13, + p10: 13, + p25: 14, + p50: 14, + p75: 15, + p90: 16, + p97_5: 18, + p99: 20, + p99_9: 29, + p99_99: 39, + p99_999: 40, + totalCount: 10526 + }, + requests: { + average: 65.79, + mean: 65.79, + stddev: 1.8, + min: 56, + max: 69, + total: 10526, + p0_001: 56, + p0_01: 56, + p0_1: 56, + p1: 59, + p2_5: 61, + p10: 64, + p25: 65, + p50: 66, + p75: 67, + p90: 67, + p97_5: 68, + p99: 69, + p99_9: 69, + p99_99: 69, + p99_999: 69, + sent: 10527 + }, + throughput: { + average: 22960.1, + mean: 22960.1, + stddev: 627.46, + min: 19549, + max: 24081, + total: 3673579, + p0_001: 19551, + p0_01: 19551, + p0_1: 19551, + p1: 20591, + p2_5: 21295, + p10: 22351, + p25: 22687, + p50: 23039, + p75: 23391, + p90: 23391, + p97_5: 23743, + p99: 24095, + p99_9: 24095, + p99_99: 24095, + p99_999: 24095 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/third.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/third.txt new file mode 100644 index 00000000..5f6ff7a7 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/oneConnection/third.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 1, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.09, + samples: 160, + start: 2023-10-31T23:09:32.072Z, + finish: 2023-10-31T23:12:12.159Z, + errors: 0, + timeouts: 0, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 10444, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 10443 }, '201': { count: 1 } }, + latency: { + average: 14.8, + mean: 14.8, + stddev: 1.33, + min: 12, + max: 33, + p0_001: 12, + p0_01: 12, + p0_1: 13, + p1: 13, + p2_5: 13, + p10: 13, + p25: 14, + p50: 15, + p75: 15, + p90: 16, + p97_5: 17, + p99: 19, + p99_9: 26, + p99_99: 31, + p99_999: 33, + totalCount: 10444 + }, + requests: { + average: 65.28, + mean: 65.28, + stddev: 2.11, + min: 57, + max: 69, + total: 10444, + p0_001: 57, + p0_01: 57, + p0_1: 57, + p1: 58, + p2_5: 60, + p10: 62, + p25: 64, + p50: 66, + p75: 67, + p90: 67, + p97_5: 68, + p99: 69, + p99_9: 69, + p99_99: 69, + p99_999: 69, + sent: 10445 + }, + throughput: { + average: 22781.6, + mean: 22781.6, + stddev: 733.9, + min: 19898, + max: 24081, + total: 3644961, + p0_001: 19903, + p0_01: 19903, + p0_1: 19903, + p1: 20255, + p2_5: 20943, + p10: 21647, + p25: 22351, + p50: 23039, + p75: 23391, + p90: 23391, + p97_5: 23743, + p99: 24095, + p99_9: 24095, + p99_99: 24095, + p99_999: 24095 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/fifth.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/fifth.txt new file mode 100644 index 00000000..26850c60 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/fifth.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.12, + samples: 160, + start: 2023-10-31T22:09:51.397Z, + finish: 2023-10-31T22:12:31.515Z, + errors: 186, + timeouts: 186, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 752, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 739 }, '201': { count: 13 } }, + latency: { + average: 2238.58, + mean: 2238.58, + stddev: 2289.75, + min: 402, + max: 9993, + p0_001: 402, + p0_01: 402, + p0_1: 402, + p1: 449, + p2_5: 474, + p10: 631, + p25: 1019, + p50: 1853, + p75: 4038, + p90: 6265, + p97_5: 8647, + p99: 9425, + p99_9: 9993, + p99_99: 9993, + p99_999: 9993, + totalCount: 752 + }, + requests: { + average: 4.7, + mean: 4.7, + stddev: 1.78, + min: 1, + max: 11, + total: 752, + p0_001: 1, + p0_01: 1, + p0_1: 1, + p1: 1, + p2_5: 2, + p10: 3, + p25: 4, + p50: 4, + p75: 5, + p90: 7, + p97_5: 9, + p99: 11, + p99_9: 11, + p99_99: 11, + p99_999: 11, + sent: 963 + }, + throughput: { + average: 1640.87, + mean: 1640.87, + stddev: 619.82, + min: 349, + max: 3844, + total: 262513, + p0_001: 349, + p0_01: 349, + p0_1: 349, + p1: 349, + p2_5: 698, + p10: 1047, + p25: 1396, + p50: 1396, + p75: 1745, + p90: 2443, + p97_5: 3141, + p99: 3839, + p99_9: 3845, + p99_99: 3845, + p99_999: 3845 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/first.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/first.txt new file mode 100644 index 00000000..1df683fd --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/first.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.13, + samples: 160, + start: 2023-10-31T22:00:53.600Z, + finish: 2023-10-31T22:03:33.728Z, + errors: 216, + timeouts: 216, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 719, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 707 }, '201': { count: 12 } }, + latency: { + average: 2489.13, + mean: 2489.13, + stddev: 2073.29, + min: 321, + max: 9849, + p0_001: 321, + p0_01: 321, + p0_1: 321, + p1: 396, + p2_5: 439, + p10: 624, + p25: 996, + p50: 1621, + p75: 3248, + p90: 5703, + p97_5: 8378, + p99: 9104, + p99_9: 9849, + p99_99: 9849, + p99_999: 9849, + totalCount: 719 + }, + requests: { + average: 4.5, + mean: 4.5, + stddev: 2.34, + min: 1, + max: 12, + total: 719, + p0_001: 1, + p0_01: 1, + p0_1: 1, + p1: 1, + p2_5: 2, + p10: 2, + p25: 3, + p50: 4, + p75: 6, + p90: 8, + p97_5: 10, + p99: 12, + p99_9: 12, + p99_99: 12, + p99_999: 12, + sent: 960 + }, + throughput: { + average: 1568.85, + mean: 1568.85, + stddev: 816.26, + min: 349, + max: 4188, + total: 250991, + p0_001: 349, + p0_01: 349, + p0_1: 349, + p1: 349, + p2_5: 698, + p10: 698, + p25: 1047, + p50: 1396, + p75: 2095, + p90: 2793, + p97_5: 3491, + p99: 4191, + p99_9: 4191, + p99_99: 4191, + p99_999: 4191 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/fourth.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/fourth.txt new file mode 100644 index 00000000..ef3efcab --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/fourth.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.12, + samples: 160, + start: 2023-10-31T22:15:16.080Z, + finish: 2023-10-31T22:17:56.195Z, + errors: 83, + timeouts: 83, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 1636, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 1621 }, '201': { count: 15 } }, + latency: { + average: 1917.82, + mean: 1917.82, + stddev: 1789.87, + min: 281, + max: 9913, + p0_001: 281, + p0_01: 281, + p0_1: 289, + p1: 344, + p2_5: 396, + p10: 460, + p25: 628, + p50: 1302, + p75: 2573, + p90: 4426, + p97_5: 6917, + p99: 8787, + p99_9: 9710, + p99_99: 9913, + p99_999: 9913, + totalCount: 1636 + }, + requests: { + average: 10.23, + mean: 10.23, + stddev: 3.14, + min: 3, + max: 16, + total: 1636, + p0_001: 3, + p0_01: 3, + p0_1: 3, + p1: 5, + p2_5: 5, + p10: 7, + p25: 8, + p50: 9, + p75: 14, + p90: 15, + p97_5: 15, + p99: 15, + p99_9: 16, + p99_99: 16, + p99_999: 16, + sent: 1744 + }, + throughput: { + average: 3569.29, + mean: 3569.29, + stddev: 1094.38, + min: 1062, + max: 5584, + total: 571039, + p0_001: 1062, + p0_01: 1062, + p0_1: 1062, + p1: 1745, + p2_5: 1745, + p10: 2443, + p25: 2793, + p50: 3141, + p75: 4887, + p90: 5235, + p97_5: 5235, + p99: 5235, + p99_9: 5587, + p99_99: 5587, + p99_999: 5587 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/second.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/second.txt new file mode 100644 index 00000000..ce624400 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/second.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.12, + samples: 160, + start: 2023-10-31T22:38:26.965Z, + finish: 2023-10-31T22:41:07.083Z, + errors: 265, + timeouts: 265, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 520, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 511 }, '201': { count: 9 } }, + latency: { + average: 2429.86, + mean: 2429.86, + stddev: 2216.1, + min: 335, + max: 9787, + p0_001: 335, + p0_01: 335, + p0_1: 335, + p1: 368, + p2_5: 429, + p10: 551, + p25: 908, + p50: 1438, + p75: 3316, + p90: 5847, + p97_5: 8860, + p99: 9427, + p99_9: 9787, + p99_99: 9787, + p99_999: 9787, + totalCount: 520 + }, + requests: { + average: 3.25, + mean: 3.25, + stddev: 2.25, + min: 1, + max: 12, + total: 520, + p0_001: 0, + p0_01: 0, + p0_1: 0, + p1: 0, + p2_5: 0, + p10: 1, + p25: 2, + p50: 2, + p75: 4, + p90: 7, + p97_5: 9, + p99: 10, + p99_9: 12, + p99_99: 12, + p99_999: 12, + sent: 810 + }, + throughput: { + average: 1134.62, + mean: 1134.62, + stddev: 784.89, + min: 349, + max: 4188, + total: 181525, + p0_001: 0, + p0_01: 0, + p0_1: 0, + p1: 0, + p2_5: 0, + p10: 349, + p25: 698, + p50: 698, + p75: 1396, + p90: 2443, + p97_5: 3151, + p99: 3491, + p99_9: 4191, + p99_99: 4191, + p99_999: 4191 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/third.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/third.txt new file mode 100644 index 00000000..1b9cd3c4 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/autocannon/twentyFiveConnections/third.txt @@ -0,0 +1,94 @@ +{ + title: undefined, + url: 'http://127.0.0.1:3000', + socketPath: undefined, + connections: 25, + sampleInt: 1000, + pipelining: 1, + workers: undefined, + duration: 160.15, + samples: 160, + start: 2023-10-31T22:22:01.327Z, + finish: 2023-10-31T22:24:41.479Z, + errors: 223, + timeouts: 223, + mismatches: 0, + non2xx: 0, + resets: 0, + '1xx': 0, + '2xx': 701, + '3xx': 0, + '4xx': 0, + '5xx': 0, + statusCodeStats: { '200': { count: 690 }, '201': { count: 11 } }, + latency: { + average: 2422.18, + mean: 2422.18, + stddev: 2116.21, + min: 387, + max: 9986, + p0_001: 387, + p0_01: 387, + p0_1: 387, + p1: 440, + p2_5: 495, + p10: 580, + p25: 1039, + p50: 1471, + p75: 3211, + p90: 5606, + p97_5: 8432, + p99: 9541, + p99_9: 9986, + p99_99: 9986, + p99_999: 9986, + totalCount: 701 + }, + requests: { + average: 4.39, + mean: 4.39, + stddev: 2.1, + min: 1, + max: 11, + total: 701, + p0_001: 0, + p0_01: 0, + p0_1: 0, + p1: 1, + p2_5: 1, + p10: 2, + p25: 3, + p50: 4, + p75: 5, + p90: 8, + p97_5: 10, + p99: 10, + p99_9: 11, + p99_99: 11, + p99_999: 11, + sent: 949 + }, + throughput: { + average: 1529.49, + mean: 1529.49, + stddev: 732.46, + min: 349, + max: 3839, + total: 244704, + p0_001: 0, + p0_01: 0, + p0_1: 0, + p1: 349, + p2_5: 349, + p10: 698, + p25: 1047, + p50: 1396, + p75: 1745, + p90: 2793, + p97_5: 3491, + p99: 3495, + p99_9: 3839, + p99_99: 3839, + p99_999: 3839 + } +} diff --git a/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/bulkload/raw-results.txt b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/bulkload/raw-results.txt new file mode 100644 index 00000000..d65cac82 --- /dev/null +++ b/docs/performance-testing/RND-644-raw-data/MaterializedConflictApproach/bulkload/raw-results.txt @@ -0,0 +1,7 @@ +Bulk load - raw results: + +00:03:28.872 +00:03:28.379 +00:03:27.527 +00:03:28.848 +00:03:27.330 diff --git a/docs/performance-testing/RND-644.md b/docs/performance-testing/RND-644.md new file mode 100644 index 00000000..17c878a8 --- /dev/null +++ b/docs/performance-testing/RND-644.md @@ -0,0 +1,45 @@ +# RND-644 - Investigate alternatives to MongoDB backend read-for-write-locking + +## Virtual Machine setup + +It's essentially a **t2.2xlarge** on AWS: + Platform: windows Server 2022. + Number of vCPUs: 8. + Storage: SSD with 80Gb. + Memory: 32 Gb. + +Before running performance tests based on the new approach: Materialize the conflict; we need to run tests based on the base code. +In other words, to come to a more solid conclusion we need to run performance tests based on the 2 approaches: + +1. Write Lock Referenced Documents approach. +2. Materialized conflict approach. + +### Write Lock Referenced Documents approach + +#### Bulk Load tool + +AVG time: 03:53:807 + +#### AutoCannon + +Ran the tests/profiling/AutocannonSchools.ts script for 160 seconds, with 1 connection and then with 25 connections. + +| | 2xxx | Non-2xxx | AVG Latency | AVG Request | AVG Throughput | +|------------------|----------|------------|-------------|-------------|----------------| +| 1 Connection | 11342 | 0 | 13.6 | 70.892 | 24739.982 | +| 25 Connections | 2745.6 | 0 | 1327.276 | 17.162 | 5990.28 | + +### Materialized conflict approach + +#### Bulk Load tool + +AVG time: 03:28:191 + +#### AutoCannon + +Ran the tests/profiling/AutocannonSchools.ts script for 160 seconds, with 1 connection and then with 25 connections. + +| | 2xxx | Non-2xxx | AVG Latency | AVG Request | AVG Throughput | +|------------------|----------|------------|-------------|-------------|----------------| +| 1 Connection | 10477 | 0 | 14.536 | 65.336 | 22591.816 | +| 25 Connections | 895.6 | 0 | 2319.514 | 5.414 | 1888.624 |