Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
47 changes: 25 additions & 22 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) {
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`,
`The Request Network SDK timed-out while polling the Request Node to confirm that the Request was persisted successfully. It is likely that the persisted Request will be confirmed eventually. App Builders are discouraged from calling persistTransaction() a second time, which would create a duplicate Request. Instead, App Builders are recommended to catch this error and continue polling the Request Node using getConfirmedTransaction() or by calling the /getConfirmedTransaction endpoint. To avoid timeouts in the future, try adjusting the httpConfig values when instantiating the RequestNetwork object: getConfirmationDeferDelay: ${this.httpConfig.getConfirmationDeferDelay}ms, getConfirmationMaxRetry: ${this.httpConfig.getConfirmationMaxRetry}, getConfirmationRetryDelay: ${this.httpConfig.getConfirmationRetryDelay}ms, getConfirmationExponentialBackoffDelay: ${this.httpConfig.getConfirmationExponentialBackoffDelay}ms, getConfirmationMaxExponentialBackoffDelay: ${this.httpConfig.getConfirmationMaxExponentialBackoffDelay}ms`,
);
}
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
25 changes: 9 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,15 @@ 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 new Promise<void>((resolve) => {
returnPersistTransaction.on('error', (e: any) => {
expect(e.message).toBe(
`The Request Network SDK timed-out while polling the Request Node to confirm that the Request was persisted successfully. It is likely that the persisted Request will be confirmed eventually. App Builders are discouraged from calling persistTransaction() a second time, which would create a duplicate Request. Instead, App Builders are recommended to catch this error and continue polling the Request Node using getConfirmedTransaction() or by calling the /getConfirmedTransaction endpoint. To avoid timeouts in the future, try adjusting the httpConfig values when instantiating the RequestNetwork object: getConfirmationDeferDelay: 0ms, getConfirmationMaxRetries: 0, getConfirmationRetryDelay: 1000ms, getConfirmationExponentialBackoffDelay: 0ms, getConfirmationMaxExponentialBackoffDelay: 30000ms`,
);
resolve();
});
});
});
});
});
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 @@ -132,37 +132,88 @@ describe('Retry', () => {
});

retry(throwFn, {
exponentialBackoffDelay: 1000,
maxExponentialBackoffDelay: 7000,
})();
retryDelay: 0,
// Exponential backoff starting at 1s, doubling each time, up to a maximum of 64s and max 8 retries, yielding a total timeout of 127s
maxRetries: 7,
exponentialBackoffDelay: 1000, // 1s
maxExponentialBackoffDelay: 64000, // 64s
})().catch(() => {
// Do nothing
});

// 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();
expect(throwFn).toHaveBeenCalledTimes(8);
expect(Date.now()).toBe(1000127000);

jest.useRealTimers();
});
Expand Down