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

Commit

Permalink
fix(personaIdentifier lock): Add lockedAt field to handle expired loc…
Browse files Browse the repository at this point in the history
…ks on personaIdentifiers (LLC-1877) (#400)
  • Loading branch information
ToranSharma authored Jul 29, 2022
1 parent 1d012e8 commit 90ad2a2
Show file tree
Hide file tree
Showing 25 changed files with 585 additions and 85 deletions.
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();
}
}
3 changes: 3 additions & 0 deletions src/errors/Locked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import BaseError from 'jscommons/dist/errors/BaseError';
import Identifier from '../models/Identifier';

export default class extends BaseError {
public readonly message: string;

constructor(public identifier: Identifier) {
super();
this.message = 'Locked';
}
}
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

0 comments on commit 90ad2a2

Please sign in to comment.