Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ec00fb2
Move getConfirmedTransaction logic to new public async function
MantisClone Jan 5, 2024
c95aef6
Adjust retry logic to make exponentialBackoffDelay more intuitive
MantisClone Jan 5, 2024
be2e95f
Update exponential backoff test
MantisClone Jan 5, 2024
29d0aaa
Update exponential backoff test
MantisClone Jan 5, 2024
5027177
Merge branch 'master' into update-default-http-config
MantisClone Jan 16, 2024
ae992bf
Merge branch 'master' into update-default-http-config
MantisClone Oct 1, 2024
bc405b3
Update retry test
MantisClone Oct 1, 2024
ca01298
Add note to README how to run tests for single package
MantisClone Oct 2, 2024
077a91f
Update error message to be more descriptive
MantisClone Oct 2, 2024
9ca0e96
Check that total elapsed time is as expected
MantisClone Oct 2, 2024
3b7801c
Update test to match new error message
MantisClone Oct 2, 2024
2a4a47b
Fix test according to CodeRabbit recommendation
MantisClone Oct 2, 2024
497f6aa
Make instruction more specific with example package name
MantisClone Oct 2, 2024
3d4b407
Fixes based on CodeRabbit comments
MantisClone Oct 2, 2024
d6339e2
Fix test per CodeRabbit comment
MantisClone Oct 2, 2024
1fa766b
Fix test
MantisClone Oct 2, 2024
7f2a5bd
update test
MantisClone Oct 2, 2024
e0c5e4b
Fix test
MantisClone Oct 2, 2024
7b76550
Update defaults
MantisClone Oct 2, 2024
a246c91
Update error message
MantisClone Oct 2, 2024
b699fbd
Clean up error messages
MantisClone Oct 2, 2024
3647631
Add a timeout to the Promise to ensure the test doesn't hang indefini…
MantisClone Oct 2, 2024
9f81a7f
Simplify and clarify the error message for better readability
MantisClone Oct 2, 2024
4aca871
Assert that it actually throws the error after it times out
MantisClone Oct 2, 2024
70e4a90
Improve error message per CodeRabbit comment
MantisClone Oct 2, 2024
9a8197c
Ensure status exists before checking for equality
MantisClone Oct 2, 2024
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ Test all the packages in the monorepo.
yarn run test
```

Test a specific package by replacing `@requestnetwork/request-client.js` with the desired package name:

```bash
yarn workspace @requestnetwork/request-client.js test
```

## License

[MIT](https://github.com/RequestNetwork/requestNetwork/blob/master/LICENSE)
Expand Down
10 changes: 6 additions & 4 deletions packages/request-client.js/src/http-config-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ const config: ClientTypes.IHttpDataAccessConfig = {
httpRequestRetryDelay: 100,
httpRequestExponentialBackoffDelay: 0,
httpRequestMaxExponentialBackoffDelay: 30000,
getConfirmationMaxRetry: 30,
getConfirmationRetryDelay: 1000,
getConfirmationExponentialBackoffDelay: 0,
getConfirmationMaxExponentialBackoffDelay: 30000,

// Exponential backoff starting at 1s, doubling after each retry, up to a maximum of 64s and max 7 retries with an initial 3s defer delay, yielding a total of 8 calls and total timeout of 130s
getConfirmationMaxRetry: 7,
getConfirmationRetryDelay: 0,
getConfirmationExponentialBackoffDelay: 1000,
getConfirmationMaxExponentialBackoffDelay: 64000,
getConfirmationDeferDelay: 3000,
};

Expand Down
49 changes: 26 additions & 23 deletions packages/request-client.js/src/http-data-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess {
{ channelId, topics, transactionData },
);

const transactionHash: string = normalizeKeccak256Hash(transactionData).value;

// Create the return result with EventEmitter
const result: DataAccessTypes.IReturnPersistTransaction = Object.assign(
new EventEmitter() as DataAccessTypes.PersistTransactionEmitter,
Expand All @@ -109,33 +107,15 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess {
// Try to get the confirmation
new Promise((r) => setTimeout(r, this.httpConfig.getConfirmationDeferDelay))
.then(async () => {
const confirmedData =
await this.fetchAndRetry<DataAccessTypes.IReturnPersistTransactionRaw>(
'/getConfirmedTransaction',
{
transactionHash,
},
{
maxRetries: this.httpConfig.getConfirmationMaxRetry,
retryDelay: this.httpConfig.getConfirmationRetryDelay,
exponentialBackoffDelay: this.httpConfig.getConfirmationExponentialBackoffDelay,
maxExponentialBackoffDelay: this.httpConfig.getConfirmationMaxExponentialBackoffDelay,
},
);
const confirmedData = await this.getConfirmedTransaction(transactionData);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this logic into a new getConfirmedTransaction() function

// when found, emit the event 'confirmed'
result.emit('confirmed', confirmedData);
})
.catch((e) => {
let error: Error = e;
if (e.status === 404) {
if (e && 'status' in e && e.status === 404) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a coderabbit suggestion.

error = new Error(
`Transaction confirmation not received. Try polling
getTransactionsByChannelId() until the transaction is confirmed.
deferDelay: ${this.httpConfig.getConfirmationDeferDelay}ms,
maxRetries: ${this.httpConfig.getConfirmationMaxRetry},
retryDelay: ${this.httpConfig.getConfirmationRetryDelay}ms,
exponentialBackoffDelay: ${this.httpConfig.getConfirmationExponentialBackoffDelay}ms,
maxExponentialBackoffDelay: ${this.httpConfig.getConfirmationMaxExponentialBackoffDelay}ms`,
`Timeout while confirming the Request was persisted. It is likely that the Request will be confirmed eventually. Catch this error and use getConfirmedTransaction() to continue polling for confirmation. Adjusting the httpConfig settings on the RequestNetwork object to avoid future timeouts. Avoid calling persistTransaction() again to prevent creating a duplicate Request.`,
);
}
result.emit('error', error);
Expand All @@ -144,6 +124,29 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess {
return result;
}

/**
* Gets a transaction from the node through HTTP.
* @param transactionData The transaction data
*/
public async getConfirmedTransaction(
transactionData: DataAccessTypes.ITransaction,
): Promise<DataAccessTypes.IReturnPersistTransaction> {
const transactionHash: string = normalizeKeccak256Hash(transactionData).value;

return await this.fetchAndRetry(
'/getConfirmedTransaction',
{
transactionHash,
},
{
maxRetries: this.httpConfig.getConfirmationMaxRetry,
retryDelay: this.httpConfig.getConfirmationRetryDelay,
exponentialBackoffDelay: this.httpConfig.getConfirmationExponentialBackoffDelay,
maxExponentialBackoffDelay: this.httpConfig.getConfirmationMaxExponentialBackoffDelay,
},
);
}

/**
* Gets the transactions for a channel from the node through HTTP.
*
Expand Down
28 changes: 12 additions & 16 deletions packages/request-client.js/test/http-data-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,18 @@ describe('HttpDataAccess', () => {
getConfirmationMaxRetry: 0,
},
});
await expect(
new Promise((resolve, reject) =>
httpDataAccess.persistTransaction({}, '', []).then((returnPersistTransaction) => {
returnPersistTransaction.on('confirmed', resolve);
returnPersistTransaction.on('error', reject);
}),
),
).rejects.toThrow(
new Error(`Transaction confirmation not received. Try polling
getTransactionsByChannelId() until the transaction is confirmed.
deferDelay: 0ms,
maxRetries: 0,
retryDelay: 1000ms,
exponentialBackoffDelay: 0ms,
maxExponentialBackoffDelay: 30000ms`),
);
const returnPersistTransaction = await httpDataAccess.persistTransaction({}, '', []);
await Promise.race([
new Promise<void>((resolve) => {
returnPersistTransaction.on('error', (e: any) => {
expect(e.message).toBe(
'Timeout while confirming the Request was persisted. It is likely that the Request will be confirmed eventually. Catch this error and use getConfirmedTransaction() to continue polling for confirmation. Adjusting the httpConfig settings on the RequestNetwork object to avoid future timeouts. Avoid calling persistTransaction() again to prevent creating a duplicate Request.',
);
resolve();
});
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('Test timed out')), 5000)),
]);
});
});
});
2 changes: 1 addition & 1 deletion packages/utils/src/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const retry = <TParams extends unknown[], TReturn>(
setTimeout(
resolve,
retryDelay +
Math.min(maxExponentialBackoffDelay, exponentialBackoffDelay * 2 ** retry),
Math.min(maxExponentialBackoffDelay, (exponentialBackoffDelay / 2) * 2 ** retry),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change adjusts the retry logic to make exponentialBackoffDelay more intuitive to configure

  • Now, 1000 means retry after 1, 2, 4, 8... seconds.
  • Previously, 1000 meant retry after 2, 4, 8, 16... seconds.

),
);

Expand Down
81 changes: 66 additions & 15 deletions packages/utils/test/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,38 +131,89 @@ describe('Retry', () => {
throw new Error(`threw`);
});

retry(throwFn, {
exponentialBackoffDelay: 1000,
maxExponentialBackoffDelay: 7000,
const retryPromise = retry(throwFn, {
retryDelay: 0,
// Exponential backoff starting at 1s, doubling after each retry, up to a maximum of 64s and max 7 retries, yielding a total of 8 call snad total timeout of 127s
maxRetries: 7,
exponentialBackoffDelay: 1000, // 1s
maxExponentialBackoffDelay: 64000, // 64s
})();

// Should call immediately
// Should call immediately (1 total calls, 0ms total elapsed)
expect(throwFn).toHaveBeenCalledTimes(1);

// Exponential backoff should only call a second time after 2000ms
jest.advanceTimersByTime(1100);
expect(Date.now()).toBe(0);

// 1st retry after 1s (2 total calls, 1000ms total elapsed)
jest.advanceTimersByTime(999);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(1100);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(2);
expect(Date.now()).toBe(1000);

// 2nd retry after 3s (3 total calls, 3000ms total elapsed)
jest.advanceTimersByTime(1999);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(2);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(3);
expect(Date.now()).toBe(3000);

// Exponential backoff should call a third time after 4100ms
jest.advanceTimersByTime(4100);
// 3rd retry after 4s (4 total calls, 7000ms total elapsed)
jest.advanceTimersByTime(3999);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(3);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(4);
expect(Date.now()).toBe(7000);

// Exponential backoff should call a fourth time after 7100ms
// since maxExponentialBackoffDelay (7000) < 8000
jest.advanceTimersByTime(7100);
// 4th retry after 8s (5 total calls, 15000ms total elapsed)
jest.advanceTimersByTime(7999);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(4);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(5);
expect(Date.now()).toBe(15000);

// Exponential backoff should call a fifth time after 7100ms
// since maxExponentialBackoffDelay (7000) < 8000
jest.advanceTimersByTime(7100);
// 5th retry after 16s (6 total calls, 31000ms total elapsed)
jest.advanceTimersByTime(15999);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(5);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(6);
expect(Date.now()).toBe(31000);

// 6th retry after 32s (7 total calls, 63000ms total elapsed)
jest.advanceTimersByTime(31999);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(6);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(7);
expect(Date.now()).toBe(63000);

// 7th retry after 64s (8 total calls, 127000ms total elapsed)
jest.advanceTimersByTime(63999);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(7);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(throwFn).toHaveBeenCalledTimes(8);
expect(Date.now()).toBe(127000);

// No further retries
jest.advanceTimersByTime(1000000000);
await Promise.resolve();
await expect(retryPromise).rejects.toThrow('threw');

expect(throwFn).toHaveBeenCalledTimes(8);
expect(Date.now()).toBe(1000127000);

jest.useRealTimers();
});
Expand Down