-
Notifications
You must be signed in to change notification settings - Fork 514
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FABN-1524: Default file checkpointer implementation (#201)
* FABN-1524: Default file checkpointer implementation Signed-off-by: Mark S. Lewis <[email protected]> * FABN-1524: Checkpoint scenario tests Signed-off-by: Mark S. Lewis <[email protected]>
- Loading branch information
1 parent
f6e8ae3
commit 0e8dfa8
Showing
18 changed files
with
368 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/** | ||
* Copyright 2020 IBM All Rights Reserved. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { Checkpointer } from '../checkpointer'; | ||
import Long = require('long'); | ||
import fs = require('fs'); | ||
|
||
const encoding = 'utf8'; | ||
|
||
interface PersistentState { | ||
blockNumber?: string; | ||
transactionIds?: string[]; | ||
} | ||
|
||
export class FileCheckpointer implements Checkpointer { | ||
private readonly path: string; | ||
private blockNumber?: Long; | ||
private transactionIds: Set<string> = new Set(); | ||
|
||
constructor(path: string) { | ||
this.path = path; | ||
} | ||
|
||
async init(): Promise<void> { | ||
await this.load(); | ||
await this.save(); | ||
} | ||
|
||
async addTransactionId(transactionId: string): Promise<void> { | ||
this.transactionIds.add(transactionId); | ||
await this.save(); | ||
} | ||
|
||
async getBlockNumber(): Promise<Long | undefined> { | ||
return this.blockNumber; | ||
} | ||
|
||
async getTransactionIds(): Promise<Set<string>> { | ||
return this.transactionIds; | ||
} | ||
|
||
async setBlockNumber(blockNumber: Long): Promise<void> { | ||
this.blockNumber = blockNumber; | ||
this.transactionIds.clear(); | ||
await this.save(); | ||
} | ||
|
||
private async load(): Promise<void> { | ||
const data = await this.readFile(); | ||
if (data) { | ||
const json = data.toString(encoding); | ||
const state = JSON.parse(json); | ||
this.setState(state); | ||
} | ||
} | ||
|
||
private async readFile(): Promise<Buffer | undefined> { | ||
try { | ||
return await fs.promises.readFile(this.path); | ||
} catch (err) { | ||
// Ignore error on non-existent file | ||
} | ||
} | ||
|
||
private setState(state: PersistentState): void { | ||
this.blockNumber = state.blockNumber ? Long.fromString(state.blockNumber) : undefined; | ||
this.transactionIds = new Set(state.transactionIds); | ||
} | ||
|
||
private async save(): Promise<void> { | ||
const state = this.getState(); | ||
const json = JSON.stringify(state); | ||
const data = Buffer.from(json, encoding); | ||
await fs.promises.writeFile(this.path, data); | ||
} | ||
|
||
private getState(): PersistentState { | ||
return { | ||
blockNumber: this.blockNumber?.toString(), | ||
transactionIds: Array.from(this.transactionIds) | ||
}; | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/** | ||
* Copyright 2020 IBM All Rights Reserved. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { Checkpointer } from '../../src/checkpointer'; | ||
import { DefaultCheckpointers } from '../../src/defaultcheckpointers'; | ||
import * as testUtils from '../testutils'; | ||
import Long = require('long'); | ||
import path = require('path'); | ||
import fs = require('fs'); | ||
import chai = require('chai'); | ||
import chaiAsPromised = require('chai-as-promised'); | ||
chai.use(chaiAsPromised); | ||
const expect = chai.expect; | ||
|
||
// tslint:disable: no-unused-expression | ||
|
||
describe('FileCheckpointer', () => { | ||
let dir: string; | ||
let file: string; | ||
let checkpointer: Checkpointer; | ||
|
||
beforeEach(async () => { | ||
dir = await testUtils.createTempDir(); | ||
file = path.join(dir, 'checkpoint.json'); | ||
checkpointer = await DefaultCheckpointers.file(file); | ||
}); | ||
|
||
afterEach(async () => { | ||
await testUtils.rmdir(dir); | ||
}); | ||
|
||
it('new checkpointer has undefined block number', async () => { | ||
const actual = await checkpointer.getBlockNumber(); | ||
|
||
expect(actual).to.be.undefined; | ||
}); | ||
|
||
it('new checkpointer has empty transaction IDs', async () => { | ||
const actual = await checkpointer.getTransactionIds(); | ||
|
||
expect(actual).to.be.empty; | ||
}); | ||
|
||
it('can get added transaction IDs', async () => { | ||
await checkpointer.addTransactionId('txId'); | ||
const actual = await checkpointer.getTransactionIds(); | ||
|
||
expect(actual).to.have.lengthOf(1).and.include('txId'); | ||
}); | ||
|
||
it('duplicate transaction IDs are ignored', async () => { | ||
await checkpointer.addTransactionId('txId'); | ||
await checkpointer.addTransactionId('txId'); | ||
const actual = await checkpointer.getTransactionIds(); | ||
|
||
expect(actual).to.have.lengthOf(1).and.include('txId'); | ||
}); | ||
|
||
it('can get updated block number', async () => { | ||
await checkpointer.setBlockNumber(Long.ONE); | ||
const actual = await checkpointer.getBlockNumber(); | ||
|
||
expect(actual?.toNumber()).to.equal(1); | ||
}); | ||
|
||
it('setting block number clears transaction IDs', async () => { | ||
await checkpointer.addTransactionId('txId'); | ||
|
||
await checkpointer.setBlockNumber(Long.ONE); | ||
const actual = await checkpointer.getTransactionIds(); | ||
|
||
expect(actual).to.be.empty; | ||
}); | ||
|
||
it('initial state retained on reopen of checkpointer', async () => { | ||
checkpointer = await DefaultCheckpointers.file(file); | ||
const blockNumber = await checkpointer.getBlockNumber(); | ||
const transactionIds = await checkpointer.getTransactionIds(); | ||
|
||
expect(blockNumber).to.be.undefined; | ||
expect(transactionIds).to.be.empty; | ||
}); | ||
|
||
it('state is persisted when block number updated', async () => { | ||
await checkpointer.setBlockNumber(Long.ONE); | ||
|
||
checkpointer = await DefaultCheckpointers.file(file); | ||
const blockNumber = await checkpointer.getBlockNumber(); | ||
const transactionIds = await checkpointer.getTransactionIds(); | ||
|
||
expect(blockNumber?.toNumber()).to.equal(1); | ||
expect(transactionIds).to.be.empty; | ||
}); | ||
|
||
it('state is persisted when transaction IDs added', async () => { | ||
await checkpointer.addTransactionId('txId'); | ||
|
||
checkpointer = await DefaultCheckpointers.file(file); | ||
const blockNumber = await checkpointer.getBlockNumber(); | ||
const transactionIds = await checkpointer.getTransactionIds(); | ||
|
||
expect(blockNumber).to.be.undefined; | ||
expect(transactionIds).to.have.lengthOf(1).and.include('txId'); | ||
}); | ||
|
||
it('persistent state is consistent on multiple updates', async () => { | ||
await checkpointer.setBlockNumber(Long.ZERO); | ||
await checkpointer.addTransactionId('tx0'); | ||
await checkpointer.setBlockNumber(Long.ONE); | ||
await checkpointer.addTransactionId('tx1'); | ||
|
||
checkpointer = await DefaultCheckpointers.file(file); | ||
const blockNumber = await checkpointer.getBlockNumber(); | ||
const transactionIds = await checkpointer.getTransactionIds(); | ||
|
||
expect(blockNumber?.toNumber()).to.equal(1); | ||
expect(transactionIds).to.have.lengthOf(1).and.include('tx1'); | ||
}); | ||
|
||
it('create fails for bad persistent data', async () => { | ||
await fs.promises.writeFile(file, Buffer.from('bad to the bone')); | ||
|
||
const promise = DefaultCheckpointers.file(file); | ||
|
||
await expect(promise).to.be.rejected; | ||
}); | ||
|
||
it('create fails for non-writable path', async () => { | ||
const promise = DefaultCheckpointers.file(path.join(dir, 'MISSING_DIR', 'MISSING_FILE')); | ||
|
||
await expect(promise).to.be.rejected; | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.