Skip to content

fix abort controller handling#576

Draft
sanderpick wants to merge 1 commit into
mainfrom
sander/fix-polling-opts
Draft

fix abort controller handling#576
sanderpick wants to merge 1 commit into
mainfrom
sander/fix-polling-opts

Conversation

@sanderpick
Copy link
Copy Markdown
Contributor

@sanderpick sanderpick commented Jul 29, 2023

Summary

Fixes abort controller handling which allows a user to abort and configure the polling timeout and interval for SDK methods that poll the gateway for a receipt.

Details

The core of this PR was adding the wiring to make the underlying polling handling aware of the user's abort controller. However, I found the API to be unintuitive and spent some time refactoring how a user goes about providing a abort-able controller with a custom timeout and polling interval. See comments below for details.

NOTE: This would be a breaking API change and therefore would require a major version bump. We probably could make the core fix while not breaking the API, but I don't think there's any need to be bashful about frequently releasing new major versions at this point. What do you guys think?

How it was tested

Updated tests to use the new API.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@sanderpick sanderpick force-pushed the sander/fix-polling-opts branch 3 times, most recently from f2191f3 to 55cf81d Compare July 31, 2023 22:27
Signed-off-by: Sander Pick <sanderpick@gmail.com>
@sanderpick sanderpick force-pushed the sander/fix-polling-opts branch from 55cf81d to b59a0db Compare July 31, 2023 22:56
Copy link
Copy Markdown
Contributor Author

@sanderpick sanderpick left a comment

Choose a reason for hiding this comment

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

@joewagner @carsonfarmer here's a draft fix for the polling behavior. Let me know what you think.

Comment thread API.md
const controller = new AbortController();
const signal = controller.signal;
mport { helpers } from "@tableland/sdk";
const controller = helpers.createPollingController(60_000, 1500); // polling timeout and interval
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here's a good view of the API. Use a helper method to create a "controller" that is passed to the .all, .run, etc. methods. The user may use this controller to abort the request via its abort method.

Comment thread src/database.ts
statements: Statement[],
opts: Signal = {}
// reads returns an Array with legnth equal to the number of batched statements,
controller?: PollingController
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Couple things here:

  1. This wasn't really an "options" object, it was a specific type of object. I usually think of "options" as an object that mixes different type of options, e.g., color, maxHeight, etc. I changed the param name to reflect.
  2. The value of opts was Signal, but this method may call mutable methods that take polling options.

Comment thread src/database.ts
return await Promise.all(
statements.map(async (stmt) => await stmt.all<T>(undefined, opts))
statements.map(
async (stmt) => await stmt.all<T>(undefined, controller)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm guessing this won't work... the timeout would be cancelled after the first result is returned. Sound right? This might need some special handling... ie, create a new controller for each statement (clone of controller) and control each of those with the single user-provider one. I can take a stab if sounds right.

Comment thread src/helpers/await.ts
}

export type SignalAndInterval = Signal & Interval;
export type PollingController = Signal & Interval;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changing this to a name that more closely describes what it does. I had to dig into the source code to figure this out when I first saw this.

Comment thread src/helpers/await.ts
abortSignal = controller.signal;
// return the timeoutId so the caller can cleanup
timeoutId = setTimeout(function () {
export function createSignal(): Signal {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Helper method for use with read-only APIs

Comment thread src/helpers/await.ts
const timeoutId = setTimeout(function () {
controller.abort();
}, timeout);
return {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This object doubles as an options object for fetch and as a controller for the user to call abort on.

Comment thread src/helpers/config.ts
): Promise<WaitableTransactionReceipt> {
if (config.autoWait ?? false) {
const waited = await receipt.wait();
const waited = await receipt.wait(controller);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the actual fix for the polling handling. wait was being called without any options / controller.

Comment thread src/lowlevel.ts
config: ReadConfig,
statement: string,
opts: Signal = {}
signal?: Signal
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed. As mentioned above for controller, this isn't really an "options" object.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We left it as options in case we wanted to add additional opts to it at a later date.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see. So you would have unioned Signal with some other type in the future? Are you suggesting I change it back?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes we could have, but naw it is probably fine. Worst case is that we have to change the parameter name in the future. But since it is already an object, it'll be a pretty safe change.

Comment thread src/statement.ts
async first<T = S, K extends keyof T = keyof T>(
colName?: K,
opts: SignalAndInterval = {}
controller?: PollingController
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is there any reason this was not optional and was instead an empty object by default. I made this change in a bunch of places.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Providing a default value makes it effectively optional. If you look at the typescript signature, it includes the ?. That way, you don't have to double check that a) opts is provided, and then b) if a given property on opts is provided. With chained ? this isn't as much of an issue, but it seemed to lead to cleaner code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

right, but if you look at the type, it doesn't have optional values anymore. you only ever need to do things like controller?.<param>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeh that's true, with those chained ? it is pretty clean. Ok!

Comment thread src/statement.ts
Comment on lines +231 to +238
await this.#waitExec(
{
type,
sql,
tables,
},
controller
);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The old opts var was being injected into the statement object, never to be seen again. Pulling the controller out into its own var makes it easier to see what's going on.

@sanderpick
Copy link
Copy Markdown
Contributor Author

Related, after poking around in here, wouldn't it be cleaner to use a traditional options object on the statement api that included the colName for filtering and the "polling controller", ie, something like ef8f74f ?

Although, maybe this is part of the codebase that has to conform to D1?

@carsonfarmer
Copy link
Copy Markdown
Member

Yeh those are D1 APIs, please don't change without ensuring they are backward compatible with 3rd party libs.

@sanderpick
Copy link
Copy Markdown
Contributor Author

Yeh those are D1 APIs, please don't change without ensuring they are backward compatible with 3rd party libs.

K, do you have a ref to the API that you're conforming to here? I see a mention of D1 on the Database object, but nothing here.

@carsonfarmer
Copy link
Copy Markdown
Member

Yeh, we should actually have a reference to its types. IIRC there was an issue a while back where we couldn't just do "implements" or "extends" because the D1 types didn't actually match the response you would get in practice. But they come from here: https://github.com/cloudflare/workerd/blob/main/types/defines/d1.d.ts and we can probably use the types from @cloudflare/worker-types now? Note that differences to these types are fine as long as they are backward compatible.

@carsonfarmer
Copy link
Copy Markdown
Member

Ooop, just noticed that all doesn't support colName anymore! So we could probably remove that to simplify the API?

@sanderpick
Copy link
Copy Markdown
Contributor Author

Cool, thanks! I see colName here: https://github.com/cloudflare/workerd/blob/main/types/defines/d1.d.ts#L26. Is that what you're looking at? Maybe it's just on first now in their API? btw, I don't have a strong pref here, it just felt weird to type undefined in the all method to be able to use the custom timeout.

@joewagner
Copy link
Copy Markdown
Contributor

@sanderpick This looks like great improvements in general. I agree the current API is very confusing, and turns out broken.
I notice you're pointing this PR at main and I was hoping to stop using this repo this week. However, in order to do that we need to get all the repos aligned on linting, prettier, and tests. I don't think there will be much issue here since the monorepo standards are mostly all being taken from this repo, but can you point this PR at the joe/lint branch?

@sanderpick
Copy link
Copy Markdown
Contributor Author

@joewagner Absolutely. Just wanted to get a first take on this. I'll try to get that done today.

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.

3 participants