Skip to content

Commit

Permalink
Wait for multiple indexes if necessary, and test.
Browse files Browse the repository at this point in the history
  • Loading branch information
autopulated committed Feb 1, 2024
1 parent a8a29e0 commit a634d5a
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 40 deletions.
72 changes: 34 additions & 38 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,11 @@ class Table {
} while (!created);

if (!tableHasRequiredIndexes) {
await this.#updateIndexes({differentIndexes, missingIndexes, existingIndexes:response.Table.GlobalSecondaryIndexes});
await this.#updateIndexes({differentIndexes, missingIndexes, existingIndexes: response.Table.GlobalSecondaryIndexes, createAll: waitForIndexes});
}

if (waitForIndexes) {
await this.#waitForIndexes();
await this.#waitForIndexesActive();
}

this[kTableIsReady] = true;
Expand Down Expand Up @@ -499,52 +499,48 @@ class Table {
return {uniqueRequiredAttributes};
}

async #updateIndexes({differentIndexes, missingIndexes, existingIndexes}) {
async #updateIndexes({differentIndexes, missingIndexes, existingIndexes, createAll}) {
if (differentIndexes.length) {
this.#logger.warn({existingIndexes, differentIndexes}, `WARNING: indexes "${differentIndexes.map(i => i.index.IndexName).join(',')}" differ from the current specifications, but these will not be automatically updated.`);
}
// FIXME: we can only add one missing index at a time, so just try to
// add the first one. Need a createIndexes option, as this could be a
// long wait if we need to create many? (Subscriber limit exceeded:
// Only 1 online index can be created or deleted simultaneously per
// table -> each update table command can only create one index....
// "You can create or delete only one global secondary index per
// UpdateTable operation.")
if (missingIndexes.length) {
const updates = {
TableName: this.name,
GlobalSecondaryIndexUpdates: [],
AttributeDefinitions: []
};
// we only need to include the attribute definitions required by
// the indexes being created, existing attribute definitions used
// by other indexes do not need to be repeated:
// FIXME see above, only adding the first missing one:
//for (const missingIndex of missingIndexes) {
// updates.GlobalSecondaryIndexUpdates.push({Create: missingIndex.index});
// updates.AttributeDefinitions = updates.AttributeDefinitions.concat(missingIndex.requiredAttributes);
//}
const missingIndex = missingIndexes.shift();
updates.GlobalSecondaryIndexUpdates.push({Create: missingIndex.index});
updates.AttributeDefinitions = updates.AttributeDefinitions.concat(missingIndex.requiredAttributes);

this.#logger.info({updates}, 'Updating table %s.', this.name);
await this[kTableDDBClient].send(new UpdateTableCommand(updates));
// Only one index can be added at a time:
for (const missingIndex of missingIndexes) {
const updates = {
TableName: this.name,
GlobalSecondaryIndexUpdates: [],
AttributeDefinitions: []
};
// we only need to include the attribute definitions required by
// the indexes being created, existing attribute definitions used
// by other indexes do not need to be repeated:
updates.GlobalSecondaryIndexUpdates.push({Create: missingIndex.index});
updates.AttributeDefinitions = updates.AttributeDefinitions.concat(missingIndex.requiredAttributes);

this.#logger.info({updates}, 'Updating table %s.', this.name);
await this[kTableDDBClient].send(new UpdateTableCommand(updates));

if (createAll) {
await this.#waitForIndexesActive();
} else {
break;
}
}
}
}

async #waitForIndexes() {
let response;
async #waitForIndexesActive() {
while (true) {
response = await this[kTableDDBClient].send(new DescribeTableCommand({TableName: this.name}));
// TODO: should be able to cover this actually
/* c8 ignore else */
if (response.Table.TableStatus === 'ACTIVE') {
const response = await this[kTableDDBClient].send(new DescribeTableCommand({TableName: this.name}));
if (response.Table.TableStatus === 'ACTIVE' &&
(response.Table.GlobalSecondaryIndexes || []).every(gsi => gsi.IndexStatus === 'ACTIVE')) {
break;
} else if (response.Table.TableStatus === 'UPDATING'){
await new Promise(resolve => setTimeout(resolve, 500));
} else if (['UPDATING', 'ACTIVE'].includes(response.Table.TableStatus) ||
(response.Table.GlobalSecondaryIndexes || []).every(gsi => ['CREATING', 'UPDATING', 'ACTIVE'].includes(gsi.IndexStatus))) {
// TODO: probably want exponential backoff here too...
await delayMs(500);
} else {
throw new Error(`Table ${this.name} status is ${response.Table.TableStatus}.`);
throw new Error(`Table ${this.name} status is ${response.Table.TableStatus}, index statuses are ${(response.Table.GlobalSecondaryIndexes || []).map(gsi => gsi.IndexStatus).join(', ')}.`);
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
],
"files": [
"test"
]
],
"timeout": 60000
},
"files": [
"index.js"
Expand Down
52 changes: 52 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,56 @@ t.test('crud:', async t => {
t.end();
});

t.test('wait for index creation', async t => {
const Schema1_noIndexes = DynamoDM.Schema('1', {
properties: {
aString: {type:'string'},
bNum: {type:'number'},
}
});
const Schema1_withIndexes = DynamoDM.Schema('1', {
properties: {
aString: {type:'string'},
bNum: {type:'number'},
}
}, {
index: {
aString:1,
indexWithHashAndSortKey: {
sortKey:'aString',
hashKey:'bNum'
}
}
});

// first create a table, and wait for its built-in indexes to be created
const table1 = DynamoDM.Table({ name: 'test-index-creation'}, {clientOptions});
const Model1a = table1.model(Schema1_noIndexes);
await table1.ready({waitForIndexes:true});

// and delete it when the test is finished
t.teardown(async () => {
await table1.deleteTable();
});

// add lots of models, so that creating the indexes for this table takes some amount of time:
for (let i = 0; i < 400; i++) {
await (new Model1a({aString:`value ${i}`, bNum:i})).save();
}
t.pass('created models ok');

// now create another reference to the existing table, which requires more indexes, and wait for them:
const table2 = DynamoDM.Table({ name: 'test-index-creation'}, {clientOptions});
const Model1b = table2.model(Schema1_withIndexes);

// wait for the indexes to be created
await table2.ready({waitForIndexes:true});

// and check that we can query using the indexes immediately:
t.equal((await Model1b.queryMany({bNum:4})).length, 1);
t.equal((await Model1b.queryMany({aString:{$gt: 'value 50'}, bNum:99})).length, 1);

t.end();
});

t.end();
2 changes: 1 addition & 1 deletion test/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ t.test('basic schemas:', async t => {
t.resolves(FooModel.testAsyncStatic(), 'async statics work');
});
});

t.test('virtuals:', async t => {
t.test('string aliases', async t => {
const table = DynamoDM.Table({ name: 'test-table-2'});
Expand Down Expand Up @@ -538,7 +539,6 @@ t.test('custom field names', async t => {
t.hasOwnProps(doc, ['myId', 'myType', 'myCreatedAt', 'myUpdatedAt']);
});


t.test('schema errors', async t => {
const table = DynamoDM.Table({ name: 'test-table-schemas'});

Expand Down

0 comments on commit a634d5a

Please sign in to comment.