diff --git a/src/server/token/MongoTokenDao.ts b/src/server/token/MongoTokenDao.ts index bb4536a..00d2ad6 100644 --- a/src/server/token/MongoTokenDao.ts +++ b/src/server/token/MongoTokenDao.ts @@ -26,6 +26,7 @@ import { TokenNamespaceData, TokenNamespaceResults, TokenQueryResult, + TokenStateHistory, TokenStatus, } from "./TokenDao"; @@ -111,6 +112,7 @@ export class MongoTokenDao implements TokenDao const token_data = { [ root + 'last' ]: token_id, + [ root + 'lastState.' + type ]: token_id, [ root + 'lastStatus' ]: token_entry, [ root + token_id + '.status' ]: token_entry, }; @@ -133,6 +135,7 @@ export class MongoTokenDao implements TokenDao new: false, fields: { [ root + 'last' ]: 1, + [ root + 'lastState' ]: 1, [ root + 'lastStatus' ]: 1, [ root + token_id + '.status' ]: 1, }, @@ -156,6 +159,7 @@ export class MongoTokenDao implements TokenDao status: token_entry, prev_status: this._getPrevStatus( prev_ns, token_id ), prev_last: this._getPrevLast( prev_ns ), + prev_state: this._getPrevState( prev_ns ), } ); } ); @@ -210,10 +214,31 @@ export class MongoTokenDao implements TokenDao status: prev_ns.lastStatus, prev_status: null, prev_last: null, + prev_state: {}, }; } + /** + * Retrieve previous token states + * + * If token state information is missing, an empty object will be + * returned. + * + * @param prev_ns previous namespace data + * + * @return previous token states + */ + private _getPrevState( + prev_ns: TokenNamespaceData | undefined + ): TokenStateHistory + { + return ( !prev_ns || prev_ns.lastState === undefined ) + ? {} + : prev_ns.lastState; + } + + /** * Retrieve existing token under the namespace NS, if any, for the doc * identified by DOC_ID @@ -233,7 +258,8 @@ export class MongoTokenDao implements TokenDao const root = this._genRoot( ns ) + '.'; const fields: any = {}; - fields[ root + 'last' ] = 1; + fields[ root + 'last' ] = 1; + fields[ root + 'lastState' ] = 1; fields[ root + 'lastStatus' ] = 1; if ( token_id ) @@ -323,6 +349,7 @@ export class MongoTokenDao implements TokenDao status: ns_data.lastStatus, prev_status: ns_data.lastStatus, prev_last: this._getPrevLast( ns_data ), + prev_state: this._getPrevState( ns_data ), }; } @@ -368,6 +395,7 @@ export class MongoTokenDao implements TokenDao status: reqtok.status, prev_status: reqtok.status, prev_last: this._getPrevLast( ns_data ), + prev_state: this._getPrevState( ns_data ), }; } diff --git a/src/server/token/Token.ts b/src/server/token/Token.ts index 1e29cff..9d4aad0 100644 --- a/src/server/token/Token.ts +++ b/src/server/token/Token.ts @@ -103,5 +103,13 @@ export interface Token * that time. */ readonly last_mismatch: boolean; + + /** + * Whether this was the most recently created token + * + * This is true iff the last token to have been in the `ACTIVE` status + * is shares the same token id. + */ + readonly last_created: boolean; } diff --git a/src/server/token/TokenDao.ts b/src/server/token/TokenDao.ts index b7f18bf..caf9804 100644 --- a/src/server/token/TokenDao.ts +++ b/src/server/token/TokenDao.ts @@ -56,19 +56,15 @@ export interface TokenDao * * The returned property depends on the actual query. */ -export interface TokenQueryResult -{ - readonly [propName: string]: TokenNamespaceResults | undefined, -} +export type TokenQueryResult = { readonly [P: string]: TokenNamespaceResults | undefined }; -/** - * Token data for requested namespaces - */ -export interface TokenNamespaceResults -{ - readonly [propName: string]: TokenNamespaceData | undefined, -} +/** Token data for requested namespaces */ +export type TokenNamespaceResults = { readonly [P: string]: TokenNamespaceData | undefined }; + + +/** Last token touching various states */ +export type TokenStateHistory = { readonly [P in TokenState]?: TokenId }; /** @@ -84,6 +80,16 @@ export interface TokenNamespaceData */ readonly last: TokenId, + /** + * Last token id to have touched each state + * + * A field representing the state will only exist if there is a token + * that last touched it. + * + * This value may not exist on older documents. + */ + readonly lastState?: TokenStateHistory, + /** * Most recent token status * @@ -98,7 +104,8 @@ export interface TokenNamespaceData * accommodate the above fields. Anything using this should cast to * `TokenEntry`. */ - readonly [propName: string]: TokenEntry | TokenStatus | TokenId | undefined, + readonly [P: string]: + TokenEntry | TokenStateHistory | TokenStatus | TokenId | undefined, } @@ -185,4 +192,13 @@ export interface TokenData * (e.g. Mongo's `findAndModify` with `new` set to `false`). */ prev_last: TokenData | null, + + /** + * Last token id to have touched each state + * + * A field representing the state will only exist if there is a token + * that last touched it. If there are no previous states, the result + * will be an empty object. + */ + prev_state: { [P in TokenState]?: TokenId }, } diff --git a/src/server/token/TokenStore.ts b/src/server/token/TokenStore.ts index 603a2e8..a886024 100644 --- a/src/server/token/TokenStore.ts +++ b/src/server/token/TokenStore.ts @@ -51,33 +51,38 @@ import { DocumentId } from "../../document/Document"; * The philosophy of this store is that any token within a given namespace * can be updated at any time, but each namespace has a unique "last" token * by document that represents the last token to have been updated within - * that context. When performing any operation on that namespace, - * information regarding that "last" token will be provided so that the - * caller can determine whether other tokens within that same context have - * been modified since a given token was last updated, which may indicate - * that a token has been superceded by another. + * that context. Also stored is a list of tokens associated with the most + * recent transition to each state. When performing any operation on that + * namespace, information regarding the last tokens will be provided so that + * the caller can determine whether other tokens within that same context + * have been modified since a given token was last updated, which may + * indicate that a token has been superceded by another. * * As an example, consider the following sequence of events within some * namespace "location" for some document 1000: * * 1. A token `A` is created for a request to a service. `last` is updated - * to point to `A`. + * to point to `A`. The last `ACTIVE` token is `A`. * * 2. The user changes information about the location. * * 3. Another token `B` is created to request information for the new - * location data. `last` is updated to point to `B`. + * location data. `last` is updated to point to `B`. The last + * `ACTIVE` token is `B`. * - * 4. The response for token `A` returns and `A` is updated. + * 4. The response for token `A` returns and `A` is updated. The last + * token in the `DONE` state is `A`. * - * 5. The caller for token `A` sees that `last` no longer points to `A` (by - * observing `last_mistmatch`), and so ignores the reply, understanding - * that `A` is now stale. + * 5. The caller for token `A` sees that the has `ACTIVE` token no longer + * points to `A` (by observing `last_created`), and so ignores the + * reply, understanding that `A` is now stale. * - * 6. The response for  `B` returns and `B` is updated. + * 6. The response for  `B` returns and `B` is updated. The last `DONE` + * token is now `B`. * - * 7. The caller notices that `last_mistmatch` is _not_ set, and so - * proceeds to continue processing token `B`. + * 7. The caller notices that `last_created` is _not_ set, and so + * proceeds to continue processing token `B`. The last token in the + * `DONE` state is now `B`. * * For more information on tokens, see `Token`. */ @@ -150,7 +155,9 @@ export class TokenStore return this._dao.updateToken( this._doc_id, this._token_ns, this._idgen(), TokenState.ACTIVE, null ) - .then( data => this._tokenDataToToken( data, TokenState.ACTIVE ) ); + .then( data => this._tokenDataToToken( + data, TokenState.ACTIVE, true + ) ); } @@ -168,7 +175,11 @@ export class TokenStore * * @return new token */ - private _tokenDataToToken( data: TokenData, state: T ): + private _tokenDataToToken( + data: TokenData, + state: T, + created: boolean = false + ): Token { return { @@ -177,6 +188,7 @@ export class TokenStore timestamp: data.status.timestamp, data: data.status.data, last_mismatch: this._isLastMistmatch( data ), + last_created: created || this._isLastCreated( data ), }; } @@ -196,6 +208,20 @@ export class TokenStore } + /** + * Whether the token represents the most recently created token + * + * @param data raw token data + * + * @return whether token was the most recently created + */ + private _isLastCreated( data: TokenData ): boolean + { + return ( data.prev_state !== undefined ) + && ( data.prev_state[ TokenState.ACTIVE ] === data.id ); + } + + /** * Complete a token * diff --git a/test/server/token/MongoTokenDaoTest.ts b/test/server/token/MongoTokenDaoTest.ts index 3bbc181..167685a 100644 --- a/test/server/token/MongoTokenDaoTest.ts +++ b/test/server/token/MongoTokenDaoTest.ts @@ -78,6 +78,9 @@ describe( 'server.token.TokenDao', () => [field]: { [ns]: { last: last_tok_id, + lastState: { + [ prev.type ]: last_tok_id, + }, lastStatus: { type: last.type, timestamp: last.timestamp, @@ -100,12 +103,16 @@ describe( 'server.token.TokenDao', () => timestamp: timestamp, data: data, }, + prev_state: { + [ prev.type ]: last_tok_id, + }, prev_status: prev, prev_last: { id: last_tok_id, status: last, prev_status: null, prev_last: null, + prev_state: {}, }, }, }, @@ -133,11 +140,13 @@ describe( 'server.token.TokenDao', () => data: data, }, prev_status: null, + prev_state: {}, prev_last: { id: last_tok_id, status: last, prev_status: null, prev_last: null, + prev_state: {}, }, }, }, @@ -158,6 +167,7 @@ describe( 'server.token.TokenDao', () => data: data, }, prev_status: null, + prev_state: {}, prev_last: null, }, }, @@ -176,6 +186,7 @@ describe( 'server.token.TokenDao', () => data: data, }, prev_status: null, + prev_state: {}, prev_last: null, }, }, @@ -192,6 +203,7 @@ describe( 'server.token.TokenDao', () => data: data, }, prev_status: null, + prev_state: {}, prev_last: null, }, }, @@ -210,9 +222,10 @@ describe( 'server.token.TokenDao', () => expect( given_data ).to.deep.equal( { $set: { - [ `${root}.last` ]: tok_id, - [ `${root}.lastStatus` ]: expected_entry, - [ `${root}.${tok_id}.status` ]: expected_entry, + [ `${root}.last` ]: tok_id, + [ `${root}.lastState.${tok_type}` ]: tok_id, + [ `${root}.lastStatus` ]: expected_entry, + [ `${root}.${tok_id}.status` ]: expected_entry, }, $push: { [ `${root}.${tok_id}.statusLog` ]: expected_entry, @@ -224,6 +237,7 @@ describe( 'server.token.TokenDao', () => new: false, fields: { [ `${root}.last` ]: 1, + [ `${root}.lastState` ]: 1, [ `${root}.lastStatus` ]: 1, [ `${root}.${tok_id}.status` ]: 1, }, @@ -298,6 +312,10 @@ describe( 'server.token.TokenDao', () => [field]: { [ns]: { last: last_tok_id, + lastState: { + [ TokenState.ACTIVE ]: last_tok_id, + [ TokenState.DONE ]: last_tok_id, + }, lastStatus: last, tok123: { @@ -311,11 +329,16 @@ describe( 'server.token.TokenDao', () => id: 'tok123', status: expected_status, prev_status: expected_status, + prev_state: { + [ TokenState.ACTIVE ]: last_tok_id, + [ TokenState.DONE ]: last_tok_id, + }, prev_last: { id: last_tok_id, status: last, prev_status: null, prev_last: null, + prev_state: {}, } }, null, @@ -369,6 +392,9 @@ describe( 'server.token.TokenDao', () => [field]: { [ns]: { last: last_tok_id, + lastState: { + [ TokenState.DEAD ]: last_tok_id, + }, lastStatus: last, [ last_tok_id ]: { @@ -382,11 +408,15 @@ describe( 'server.token.TokenDao', () => id: last_tok_id, status: last, prev_status: last, + prev_state: { + [ TokenState.DEAD ]: last_tok_id, + }, prev_last: { id: last_tok_id, status: last, prev_status: null, prev_last: null, + prev_state: {}, } }, null, @@ -430,6 +460,7 @@ describe( 'server.token.TokenDao', () => { const expected_fields = { [ `${field}.${ns}.last` ]: 1, + [ `${field}.${ns}.lastState` ]: 1, [ `${field}.${ns}.lastStatus` ]: 1, }; diff --git a/test/server/token/TokenStoreTest.ts b/test/server/token/TokenStoreTest.ts index 76e97af..9ed12a8 100644 --- a/test/server/token/TokenStoreTest.ts +++ b/test/server/token/TokenStoreTest.ts @@ -82,6 +82,7 @@ describe( 'TokenStore', () => timestamp: expected_ts, data: expected_data, last_mismatch: false, + last_created: false, }, ], @@ -116,6 +117,34 @@ describe( 'TokenStore', () => timestamp: expected_ts, data: expected_data, last_mismatch: true, + last_created: false, + }, + ], + + [ + "returns existing token with set last created", + { + id: token_id, + + status: { + type: TokenState.DEAD, + timestamp: expected_ts, + data: expected_data, + }, + + prev_status: null, + prev_last: null, + prev_state: { + [ TokenState.ACTIVE ]: token_id, + }, + }, + { + id: token_id, + state: TokenState.DEAD, + timestamp: expected_ts, + data: expected_data, + last_mismatch: true, + last_created: true, }, ], ] ).forEach( ( [ label, dbdata, expected ] ) => it( label, () => @@ -211,6 +240,8 @@ describe( 'TokenStore', () => prev_status: null, prev_last: null, }, + + prev_state: {}, }, { id: token_id, @@ -218,6 +249,7 @@ describe( 'TokenStore', () => timestamp: expected_ts, data: expected_data, last_mismatch: true, + last_created: true, }, ], @@ -233,6 +265,7 @@ describe( 'TokenStore', () => prev_status: null, prev_last: null, + prev_state: {}, }, { id: token_id, @@ -240,6 +273,7 @@ describe( 'TokenStore', () => timestamp: expected_ts, data: expected_data, last_mismatch: true, + last_created: true, }, ], ] ).forEach( ( [ label, dbdata, expected ] ) => it( label, () => @@ -290,6 +324,7 @@ describe( 'TokenStore', () => timestamp: 0, data: "", last_mismatch: true, + last_created: true, }, "complete-data", { @@ -298,6 +333,7 @@ describe( 'TokenStore', () => timestamp: expected_ts, data: "complete-data", last_mismatch: true, + last_created: true, }, ], @@ -309,6 +345,7 @@ describe( 'TokenStore', () => timestamp: 0, data: "accept", last_mismatch: true, + last_created: true, }, "accept-data", { @@ -317,6 +354,7 @@ describe( 'TokenStore', () => timestamp: expected_ts, data: "accept-data", last_mismatch: true, + last_created: true, }, ], @@ -328,6 +366,7 @@ describe( 'TokenStore', () => timestamp: 0, data: "kill", last_mismatch: true, + last_created: true, }, "kill-data", { @@ -336,6 +375,7 @@ describe( 'TokenStore', () => timestamp: expected_ts, data: "kill-data", last_mismatch: true, + last_created: true, }, ], ] ).forEach( ( [ method, token, data, expected ] ) => describe( `#${method}`, () => @@ -378,6 +418,9 @@ describe( 'TokenStore', () => prev_status: null, prev_last: null, + prev_state: { + [ TokenState.ACTIVE ]: token.id, + }, } ); } }();