Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp retry and remove unintended exports #69

Merged
merged 6 commits into from
Apr 23, 2021
Merged

Conversation

simonz-bq
Copy link
Contributor

  • Refactored retry logic
  • Handle retrying on failures during StartSession
  • Removed some exports and accessibility to non intentional and undocumented functions and types
  • Bumped aws-sdk dependency requirement to include CapacityExceededException
  • Bumped driver version to 2.2.0 and added changelog

},
"peerDependencies": {
"aws-sdk": "^2.815.0",
"aws-sdk": "^2.841.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

any reason we aren't bumping the devDependency aws-sdk to the same verison?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will fix

* Removed `Transaction` from the exports list.
* Removed modules being accessible when importing from `amazon-qldb-driver-nodejs/src`.
* TypeScript: Removed constructors in the API for some exported types.

# 2.1.1
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add the date for when 2.1.1 was released

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we stopped doing that. The release date is also available on npm repository.

@@ -34,10 +34,10 @@
"sinon": "^7.3.2",
"ts-node": "^8.3.0",
"typedoc": "^0.15.0",
"typescript": "^3.5.3"
"typescript": "^3.8.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be called out in CHANGELOG?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a dev dependency so it doesn't matter to users

@@ -48,13 +54,15 @@ export function info(line: string): void {
/**
* @returns A boolean indicating whether a logger has been set within the AWS SDK.
*/
Copy link
Contributor

@byronlin13 byronlin13 Apr 15, 2021

Choose a reason for hiding this comment

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

do we need @internal here?

Same with _prepend

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, because otherwise these are exported to the API


retryConfig = (retryConfig == null) ? this._retryConfig : retryConfig;
let session: QldbSession;
let startNewSession: boolean = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this correct?

For the happy case where an execute is successful, the session is placed back into the pool. On another execute, it will create a new session instead of picking up the valid session based on getSession logic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

was fixed already but I didnt push it up yet

if (e.isRetriable) {
// Always retry on the first attempt if failure was caused by a stale session in the pool
if (retryAttempt == 1 && e.isISE) {
debug("Initial session received from pool invalid. Retrtying...");
Copy link
Contributor

@byronlin13 byronlin13 Apr 15, 2021

Choose a reason for hiding this comment

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

Initial session received from pool is invalid. Retrying...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@@ -19,6 +19,8 @@ const HASH_SIZE: number = 32

Copy link
Contributor

@byronlin13 byronlin13 Apr 15, 2021

Choose a reason for hiding this comment

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

Should we add @internal to HASH SIZE? I see it in the docs

Screen Shot 2021-04-15 at 2 48 51 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not exported in the typescript definition though.

* An InvalidSessionException indicates that the parent session is dead, and a new transaction cannot be created
* without a new (Pooled)QldbSession being created from the parent driver.
* Every transaction is tied to a parent QldbSession, meaning that if the parent session is closed or invalidated, the
* child transaction is automatically closed and cannot be used. Only one transaction can be active at any given time
Copy link
Contributor

Choose a reason for hiding this comment

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

at any

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

* Fixed dev dependency version not matching peer dependency
* Fixed a typo in docstrings
* Updated README TypeScript versioning to match package.json
@@ -156,36 +169,95 @@ export class QldbDriver {
* @throws {@linkcode InvalidSessionException} When a session expires either due to a long running transaction or session being idle for long time.
* @throws {@linkcode BadRequestException} When Amazon QLDB is not able to execute a query or transaction.
*/
async executeLambda(
transactionLambda: (transactionExecutor: TransactionExecutor) => any,
async executeLambda<Type>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather make this a Promise<T>. Using Type like this is confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

}

// Acquire semaphore and get a session from the pool
const getSession: (thisDriver: QldbDriver, startNewSession: boolean) => Promise<QldbSession> =
Copy link
Contributor

Choose a reason for hiding this comment

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

This extra typing information is probably unnecessary/can be inferred.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.


// Release semaphore and if the session is alive return it to the pool and return true
const releaseSession: (thisDriver: QldbDriver, session: QldbSession) => boolean =
function(thisDriver: QldbDriver, session: QldbSession): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

Typing information can be inferred

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

retryConfig = (retryConfig == null) ? this._retryConfig : retryConfig;
let session: QldbSession;
let startNewSession: boolean = false;
for (let retryAttempt: number = 1; true; retryAttempt++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Eh? Why not a simple while(true) or for (blah < whatever).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well we want to instantiate the retryAttempt and have it increment on every loop, so why not?

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks unusual to me, which doesn't seem worth it for saving one line of code. Your call to change or not.

return await session.executeLambda(transactionLambda);
} catch (e) {
if (e instanceof ExecuteError) {
if (e.isRetriable) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we introduced this odd spelling here, let's not do that, otherwise I'll just be sad for a bit :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll immediately investigate if this can be done (see if we don't have any public facing API functions that say "retriable") after this PR is completed.

Choose a reason for hiding this comment

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

Thanks - can you create a GitHub issue to track this improvement?

}
await new Promise(resolve => setTimeout(resolve, backoffDelay));

info(`A recoverable error has occured. Attempting retry #${retryAttempt}.`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Debug-level I think

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The rest of the drivers have this on the info level.

Copy link
Contributor

Choose a reason for hiding this comment

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

*occurred

throw new StartTransactionError(e);
const isRetriable: boolean = isRetriableException(e);
const isISE: boolean = isInvalidSessionException(e);
if (isISE && !isTransactionExpiredException(e)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Again expand the acronym please

Copy link
Contributor Author

@simonz-bq simonz-bq Apr 20, 2021

Choose a reason for hiding this comment

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

We cant do that here because isInvalidSessionException is imported from another module, so it's already defined.

Choose a reason for hiding this comment

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

What @alpian is referring to is the isISE variable, we should expand that to not be the abbrevation.

Choose a reason for hiding this comment

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

What was missed in the response here is that we cannot expand the name to match the function due to name resolution, and the feeling of keeping this local variable as an acronym would be acceptable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Confused. Why can't const isISE be const isSessionInvalid?

Object.setPrototypeOf(this, ExecuteError.prototype)
this.cause = cause;
this.isRetriable = isRetriable;
this.isISE = isISE;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing here, what is ISE?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -1,5 +1,5 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is muddying the PR. Can we keep this cosmetic stuff separate please?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was an intentional decision since almost all of the files were being touched anyways.

In the future will keep in a separate PR!

@@ -1,13 +1,14 @@
{
"compilerOptions": {
"target": "es6",
"target": "ES2015",
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do this? These are the same afaik,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consistency. We had a reference to es2015 and es6 in the same file, which can be somewhat confusing.

Official TS docs also says the "allowed" string is capitalized, so might as well do an easy clean-up.

@@ -15,12 +15,12 @@ from the ~/.aws/config file.

See [Setting Region](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-region.html) page for more information.

### TypeScript 3.5.x
### TypeScript 3.8.x

Choose a reason for hiding this comment

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

Is 3.8 needed, or can it stay at 3.5?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Honestly we don't even need 3.5. The real version we need is some undetermined previous version.

Figured we'd align up our documentation and all and 3.8 was what I was playing with testing hash private functions. This ultimately doesn't matter because it's an independent dev dependency.

session.endSession();
}
}
this._isClosed = true;

Choose a reason for hiding this comment

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

We should probably mark the driver as closed before starting to empty the pool so no new items can be opened (no matter that this shouldn't be possible given the single-threaded nature).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, but yes it doesn't matter in this case.

close(): void {
while (this._sessionPool.length > 0) {
const session: QldbSession = this._sessionPool.pop();
if (session != undefined) {

Choose a reason for hiding this comment

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

Is this possible, that there can be undefined items in the pool?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not 100% sure as node is supposedly single threaded so this code block shouldn't have multiple things accessing it, but popping from an empty array gives you undefined. This safety check is a case of better safe than sorry.

`Getting session. Current free session count: ${thisDriver._sessionPool.length}. ` +
`Currently available permit count: ${thisDriver._availablePermits}.`
);
if (thisDriver._semaphore.tryAcquire()) {

Choose a reason for hiding this comment

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

This is purely stylistic, but when there's an error case my preference is to put it within the if block and have the throw there, and leave the rest of the code not indented. Here, this would mean checking on !tryAcquire() and throwing, while leaving the rest of the code not indented.

Purely stylistic.

let startNewSession: boolean = false;
for (let retryAttempt: number = 1; true; retryAttempt++) {
try {
session = null;

Choose a reason for hiding this comment

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

Why the assignment to null before assigning on getSession?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Prevents the session from the last loop being passed to releaseSession twice in the case that startSession fails

if (e.isRetriable) {
// Always retry on the first attempt if failure was caused by a stale session in the pool
if (retryAttempt == 1 && e.isISE) {
debug("Initial session received from pool is invalid. Retrtying...");

Choose a reason for hiding this comment

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

Note the typo in Retrying...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

throw e.cause;
}

const backoffFunction: BackoffFunction = retryConfig.getBackoffFunction();

Choose a reason for hiding this comment

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

Deprecation would need a notice for at least a release, and can be done via: microsoft/TypeScript#390

} catch (e) {
if (isBadRequestException(e)) {
throw new StartTransactionError(e);
const isRetriable: boolean = isRetriableException(e);

Choose a reason for hiding this comment

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

I think this spelling makes Ian sad.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. Let me check after this PR whether or not we exposed any function names or parameters as "Retriable" to see if we can fix this.

return new Transaction(this._communicator, startTransactionResult.TransactionId);
}

private async _tryAbort(): Promise<void> {

Choose a reason for hiding this comment

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

Generally a tryX function would return true or false depending on if it was successful at the try or not.

@@ -167,8 +165,20 @@ export function isBadRequestException(e: AWSError): boolean {
* Is the exception a retriable exception?
* @param e The client error caught.
* @returns True if the exception is a retriable exception. False otherwise.
*
* @internal
*/
export function isRetriableException(e: AWSError): boolean {

Choose a reason for hiding this comment

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

Given that these are no longer exported, we're probably free to change the spelling?

Choose a reason for hiding this comment

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

Perhaps adding a separate PR for cosmetic items, as Ian suggested below (dates and spelling changes).

* Infer type of scoped functions in execute
* Expand IsISE to full name
* Rename tryAbort since it doesn't return a bool
* Fix a typo in log message
@simonz-bq simonz-bq marked this pull request as ready for review April 20, 2021 22:50
Copy link

@kylepbit kylepbit left a comment

Choose a reason for hiding this comment

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

+1

Copy link
Contributor

@alpian alpian left a comment

Choose a reason for hiding this comment

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

Approved with comments

retryConfig = (retryConfig == null) ? this._retryConfig : retryConfig;
let session: QldbSession;
let startNewSession: boolean = false;
for (let retryAttempt: number = 1; true; retryAttempt++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks unusual to me, which doesn't seem worth it for saving one line of code. Your call to change or not.

throw new StartTransactionError(e);
const isRetriable: boolean = isRetriableException(e);
const isISE: boolean = isInvalidSessionException(e);
if (isISE && !isTransactionExpiredException(e)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Confused. Why can't const isISE be const isSessionInvalid?

@simonz-bq simonz-bq merged commit 0b5c02d into master Apr 23, 2021
@simonz-bq simonz-bq deleted the revamp-retry branch April 23, 2021 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants