Skip to content
This repository has been archived by the owner on May 5, 2023. It is now read-only.

fix(personaIdentifier lock): Add lockedAt field to handle expired locks on personaIdentifiers (LLC-1877) #400

Merged
merged 17 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from 12 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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ MODELS_REPO=memory
STORAGE_REPO=memory
DEFAULT_TIMEOUT_MS=300000

# Persona Identifier lock expiration time in ms (default 30000)
#IDENTIFIER_LOCK_EXPIRATION_MS=30000

# Winston
WINSTON_CONSOLE_LEVEL=info
WINSTON_CLOUDWATCH_ENABLED=false
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const getMongoNumberOption = (option?: string) => {
return defaultTo<number|undefined>(Number(option), undefined);
};

const DEFAULT_IDENTIFIER_LOCK_EXPIRATION_MS = 30000; // 30 seconds.
export const IDENTIFIER_LOCK_EXPIRATION_MS = getNumberOption(
process.env.IDENTIFIER_LOCK_EXPIRATION_MS,
DEFAULT_IDENTIFIER_LOCK_EXPIRATION_MS,
);

export default {
defaultTimeout: getNumberOption(process.env.DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS),
lang: getStringOption(process.env.LANG, 'en'),
Expand Down
11 changes: 11 additions & 0 deletions src/errors/ExpiredLock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import BaseError from 'jscommons/dist/errors/BaseError';
import Identifier from '../models/Identifier';

export class ExpiredLock extends BaseError {
constructor(
public identifier: Identifier,
public ignorePersonaId: boolean,
) {
super();
}
}
4 changes: 4 additions & 0 deletions src/models/Identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ interface Identifier extends BaseModel {
readonly ifi: Ifi;
}

export interface IdentifierWithPersona extends Identifier {
readonly persona: string;
}

export default Identifier;
70 changes: 44 additions & 26 deletions src/mongoModelsRepo/createUpdateIdentifierPersona.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import NoModel from 'jscommons/dist/errors/NoModel';
import { ObjectID } from 'mongodb';
import * as promiseRetry from 'promise-retry';

import { ExpiredLock } from '../errors/ExpiredLock';
import Locked from '../errors/Locked';
import CreateUpdateIdentifierPersonaOptions // tslint:disable-line:import-spacing
from '../repoFactory/options/CreateUpdateIdentifierPersonaOptions';
Expand All @@ -9,11 +12,15 @@ import CreateUpdateIdentifierPersonaResult // tslint:disable-line:import-spacing
import GetIdentifierResult from '../repoFactory/results/GetIdentifierResult';
import Lockable from '../repoFactory/utils/Lockable';
import Config from './Config';
import createPersona from './createPersona';
import getTheIdentifier from './getIdentifier';
import getIdentifierByIfi from './getIdentifierByIfi';
import setIdentifierPersona from './setIdentifierPersona';
import createIdentifier from './utils/createIdentifier';
import createOrUpdateIdentifier from './utils/createOrUpdateIdentifier';
import { createPersonaAndAddToIdentifier } from './utils/createPersonaAndAddToIdentifier';

type TheCreateUpdateIdentifierPersonaOptions = CreateUpdateIdentifierPersonaOptions & {
readonly getIdentifier?: (opts: GetIdentifierOptions) => Promise<GetIdentifierResult & Lockable>;
};

const create = (config: Config) =>
async ({
Expand All @@ -32,23 +39,10 @@ const create = (config: Config) =>
throw new Locked(identifier);
}

const { persona } = await createPersona(config)({
name: personaName,
organisation,
});

const { identifier: updatedIdentifier } = await setIdentifierPersona(config)({
id: identifier.id,
organisation,
persona: persona.id,
return await createPersonaAndAddToIdentifier(config)({
identifier,
personaName,
});

return {
identifier: updatedIdentifier,
identifierId: identifier.id,
personaId: persona.id,
wasCreated,
};
};

const createUpdateIdentifierPersona = (config: Config) =>
Expand All @@ -70,7 +64,7 @@ const createUpdateIdentifierPersona = (config: Config) =>
organisation,
});

if ( locked === true ) {
if (locked === true) {
// We are locked, wait for unlock
throw new Locked(foundIdentifier);
}
Expand All @@ -85,8 +79,8 @@ const createUpdateIdentifierPersona = (config: Config) =>
// currently it doesn't get updated

return {
identifier: foundIdentifier,
identifierId,
identifier: foundIdentifier,
personaId: foundIdentifier.persona,
wasCreated: false,
};
Expand All @@ -99,15 +93,38 @@ const createUpdateIdentifierPersona = (config: Config) =>
personaName,
});
}
if (err instanceof ExpiredLock) {
const {
identifier: expiredIdentifier,
ignorePersonaId,
} = err;

const { identifier: identifierWithoutPersona } = await createOrUpdateIdentifier(config)({
filter: {
_id: new ObjectID(expiredIdentifier.id),
organisation: new ObjectID(organisation),
},
update: {
$set: { lockedAt: new Date() },
...(
ignorePersonaId
? { $unset: { persona: '' } }
: {}
),
},
upsert: false,
});

return await createPersonaAndAddToIdentifier(config)({
identifier: identifierWithoutPersona,
personaName,
});
}

throw err;
}

};

type TheCreateUpdateIdentifierPersonaOptions = CreateUpdateIdentifierPersonaOptions & {
readonly getIdentifier?: (opts: GetIdentifierOptions) => Promise<GetIdentifierResult & Lockable>;
};

const retryCreateUpdateIdentifierPersona = (config: Config) =>
async (opts: TheCreateUpdateIdentifierPersonaOptions):
Promise<CreateUpdateIdentifierPersonaResult> => {
Expand All @@ -116,7 +133,8 @@ const retryCreateUpdateIdentifierPersona = (config: Config) =>

return promiseRetry<CreateUpdateIdentifierPersonaResult>(async (retry) => {
try {
return await createUpdateIdentifierPersonaFn(opts);
const res = await createUpdateIdentifierPersonaFn(opts);
return res;
} catch (err) {
/* istanbul ignore else */
if (err instanceof Locked) {
Expand Down
3 changes: 1 addition & 2 deletions src/mongoModelsRepo/getAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ export default (config: Config) => {
id: document._id.toString(),
key: document.key,
organisation: document.organisation.toString(),
/* istanbul ignore next */ // shouldnt be null..
personaId: document.personaId === null ? null : document.personaId.toString(),
personaId: document.personaId?.toString(),
value: document.value,
};
return { attribute };
Expand Down
16 changes: 13 additions & 3 deletions src/mongoModelsRepo/getIdentifier.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import NoModel from 'jscommons/dist/errors/NoModel';
import { ObjectID } from 'mongodb';

import { IDENTIFIER_LOCK_EXPIRATION_MS } from '../config';
import { ExpiredLock } from '../errors/ExpiredLock';
import Identifier from '../models/Identifier';
import GetIdentifierOptions from '../repoFactory/options/GetIdentifierOptions';
import GetIdentifierResult from '../repoFactory/results/GetIdentifierResult';
import Lockable from '../repoFactory/utils/Lockable';
Expand All @@ -22,13 +26,19 @@ export default (config: Config) => {
throw new NoModel('Identifier');
}

const identifier = {
const identifier: Identifier = {
id: document._id.toString(),
ifi: document.ifi,
organisation: document.organisation.toString(),
/* istanbul ignore next */ // shouldnt be null..
persona: document.persona === null ? null : document.persona.toString(),
persona: document.persona?.toString(),
};

const lockAge = (new Date()).getTime() - (document.lockedAt?.getTime() ?? 0);

if (document.locked && lockAge > IDENTIFIER_LOCK_EXPIRATION_MS) {
throw new ExpiredLock(identifier, document.lockedAt === undefined);
}

return { identifier, locked: document.locked };
};
};
3 changes: 1 addition & 2 deletions src/mongoModelsRepo/getIdentifierByIfi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export default (config: Config) => {
}

const identifierId = document._id.toString();
/* istanbul ignore next */ // shouldnt be null..
const personaId = document.persona === null ? null : document.persona.toString();
const personaId = document.persona?.toString();
return { identifierId, personaId };
};
};
3 changes: 1 addition & 2 deletions src/mongoModelsRepo/getPersonaAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ export default (config: Config) => {
id: document._id.toString(),
key: document.key,
organisation: document.organisation.toString(),
/* istanbul ignore next */ // shouldnt be null..
personaId: document.personaId === null ? null : document.personaId.toString(),
personaId: document.personaId?.toString(),
value: document.value,
}));

Expand Down
3 changes: 1 addition & 2 deletions src/mongoModelsRepo/getPersonaIdentifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ export default (config: Config) => {
id: document._id.toString(),
ifi: document.ifi,
organisation: document.organisation.toString(),
/* istanbul ignore next */ // shouldnt be null..
persona: document.persona === null ? null : document.persona.toString(),
persona: document.persona?.toString(),
}));

const identifiers = await formattedDocuments.toArray();
Expand Down
3 changes: 3 additions & 0 deletions src/mongoModelsRepo/overwriteIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export default (config: Config) => {
ifi,
organisation: new ObjectID(organisation),
},
$unset: {
lockedAt: '',
},
};

return createOrUpdateIdentifier(config)({
Expand Down
5 changes: 4 additions & 1 deletion src/mongoModelsRepo/setIdentifierPersona.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ export default (config: Config) => {
locked: false,
persona: new ObjectID(persona),
},
$unset: {
lockedAt: '',
},
};

return await createOrUpdateIdentifier(config)({
filter,
update,
upsert: false,
});
}) as SetIdentifierPersonaResult;
};
};
72 changes: 72 additions & 0 deletions src/mongoModelsRepo/tests/createPersonaAndAddToIdentifier.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as assert from 'assert';
import {
MongoClient,
} from 'mongodb';

import config from '../../config';
import repoFactory from '../../repoFactory';
import ServiceConfig from '../../service/Config';
import { TEST_IFI, TEST_ORGANISATION } from '../../tests/utils/values';
import createUpdateIdentifierPersona from '../createUpdateIdentifierPersona';
import creatIdentifier from '../utils/createIdentifier';
import { createPersonaAndAddToIdentifier } from '../utils/createPersonaAndAddToIdentifier';

describe('', async () => {

let serviceConfig: ServiceConfig; // tslint:disable-line:no-let
beforeEach(async () => {
const repoFacade = repoFactory();
serviceConfig = {repo: repoFacade};
await serviceConfig.repo.clearRepo();
});

const generateMockDb = async () => {
return (await MongoClient.connect(
config.mongoModelsRepo.url,
config.mongoModelsRepo.options,
)).db();
};

it(
'Should create a new persona and add toidentifier if identifier doens\'t have one',
async () => {
const dbConfig = { db: generateMockDb() };

const { identifier: testNewIdentifier } = await creatIdentifier(dbConfig)({
ifi: TEST_IFI,
organisation: TEST_ORGANISATION,
});

const {
identifier: identifierWithPersona,
wasCreated,
} = await createPersonaAndAddToIdentifier(dbConfig)({
identifier: testNewIdentifier,
personaName: 'David Tennant',
});

assert.equal(identifierWithPersona.persona !== undefined, true, 'Should create and add persona to identifier');
assert.equal(wasCreated, true, 'Should create a new persona document');
});

it('Should just return identifier unchanged if it already has persona', async () => {
const dbConfig = { db: generateMockDb() };

const { identifier: identifierWithPersona } = await createUpdateIdentifierPersona(dbConfig)({
ifi: TEST_IFI,
organisation: TEST_ORGANISATION,
personaName: 'David Tester',
});

const {
identifier: unchangedIdentifier,
wasCreated,
} = await createPersonaAndAddToIdentifier(dbConfig)({
identifier: identifierWithPersona,
personaName: 'Dave Tester',
});

assert.deepEqual(unchangedIdentifier, identifierWithPersona, 'Identifier should be unchanged');
assert.equal(wasCreated, false, 'We shouldn\'t have created anything, so should be false');
});
});
Loading