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

feat!: dont batch queries #65

Merged
merged 1 commit into from
Dec 14, 2023
Merged

feat!: dont batch queries #65

merged 1 commit into from
Dec 14, 2023

Conversation

samsiegart
Copy link
Contributor

refs Agoric/agoric-sdk#8505

We've already switched from using the batch RPC API to JSON REST API in #55. But, we were still using a Promise.all() with the new interface, effectively batching those queries together. This PR makes each vstorage path poll asynchronously on separate timers.

Importantly, this changes the error handling to be more manageable. Instead of having a separate error handler for each watchLatest, it just uses a single onError callback given to chainStorageWatcher constructor. The new JSON API doesn't include an optional error message+code in response objects--it's limited to just http-level error codes, and in the case of missing vstorage data, an empty value in the response. This allows us to use axios to automatically retry on 500-level errors a couple times before invoking onError.

This updated error handling allows for clients to more easily manage faulty RPC nodes. Instead of adding an error handler to every place watchLatest is used, and use the result to decipher whether the vstorage data is missing or the RPC node is failing, it can just use the single onError callback on the chainStorageWatcher, and trust that the chainStorageWatcher already retried a couple times. This gives the client a reliable way to know when to notify the user of an RPC outage and/or prompt them to select a different node (assuming a node selector is available).

For queryOnce, it also no longer batches with other requests, and uses axios to retry before throwing an error, more similar to a regular fetch request.

Added some test cases for the updated error handling.

Copy link
Member

@turadg turadg left a comment

Choose a reason for hiding this comment

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

Thanks for the detailed explanation in the PR body

Comment on lines 86 to 87
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
Copy link
Member

Choose a reason for hiding this comment

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

let's do our best to avoid lint suppressions. isn't it unknown?

Suggested change
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
} catch (e: unknown) {

Then onError reports that it takes only an Error. If that's the case, then here we should check that it's really an Error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yea that works, done.

isQueryInProgress = false;
queueNextQuery();
const { value, blockHeight } = response;
const lastValue = latestValueCache.get(pathKey);
Copy link
Member

Choose a reason for hiding this comment

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

use of the lastValue tuple is a little confusing. In fact, there's a bug in using the [0] (identifier) for both comparisons.

To avoid that please destructure instead.

Suggested change
const lastValue = latestValueCache.get(pathKey);
// 'identifier' is block height because this is a *data* query
const [ latestValueHeight, latestValue ] = latestValueCache.get(pathKey) || [];

Alternately, make a getLatestDataValue() helper that returns { value, blockHeight } like the response does. Then the condition could be like:

if (response.blockHeight === cached.blockHeight || (response.blockHeight === undefined && response.value  === cached.value))

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 just made a type for LatestValueIdentifier with a doc comment to hopefully clarify, and used the array destructuring you suggested

packages/rpc/src/chainStorageWatcher.ts Show resolved Hide resolved

// Assumes argument is an unserialized presence.
const presenceToSlot = (o: unknown) => marshaller.toCapData(o).slots[0];

const queryBoardAux = <T>(boardObjects: unknown[]) => {
const queryBoardAux = (boardObjects: unknown[]) => {
Copy link
Member

Choose a reason for hiding this comment

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

why lose the generic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just added back, but I had to make vstorageQuery generic as well

node: string,
unmarshal: FromCapData<string>,
path: [AgoricChainStoragePathKind, string],
) => {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
) => {
): { blockHeight?: number, value: unknown } => {

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 (except had to make value a generic and the return value a Promise)

@@ -1,11 +1,12 @@
/* eslint-disable no-use-before-define */
/* eslint-disable import/extensions */
import { expect, it, describe, beforeEach, vi, afterEach } from 'vitest';
// eslint-disable-next-line import/no-extraneous-dependencies
Copy link
Member

Choose a reason for hiding this comment

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

test is part of devDependencies so the lint rules should allow it. You may need to tell the eslintconfig that test allows devDependencies: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-extraneous-dependencies.md#options

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems like it works now, I think my vscode linter was just deceiving me or not picking up the package.json changes

Comment on lines 355 to 356
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new Promise<any[]>(res => {
Copy link
Member

Choose a reason for hiding this comment

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

does this work?

Suggested change
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new Promise<any[]>(res => {
new Promise<unknown[]>(res => {

not a blocker since this is a test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm now it does, just changed. Before I was getting some issue with 'axios-mock-adapter' expecting Promise<any>

Comment on lines 94 to 96
if (e instanceof Error) {
onError && onError(e);
}
Copy link
Member

Choose a reason for hiding this comment

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

consider one conditional expression,

Suggested change
if (e instanceof Error) {
onError && onError(e);
}
if (onError && e instanceof Error) {
onError(e);
}

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

latestValue &&
(blockHeight === latestValueIdentifier ||
// Blockheight is undefined so fallback to using the stringified value
// as the identifier, as is the case for `children` queries.
Copy link
Member

Choose a reason for hiding this comment

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

helpful comment

export const vstorageQuery = async <T>(
node: string,
unmarshal: FromCapData<string>,
path: [AgoricChainStoragePathKind, string],
Copy link
Member

Choose a reason for hiding this comment

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

consider naming these so they're clearer below

Suggested change
path: [AgoricChainStoragePathKind, string],
[pathKind, path]: [AgoricChainStoragePathKind, string],

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

Comment on lines 57 to 60
const latestValue =
typeof data.values !== 'undefined'
? parseIfJSON(data.values[data.values.length - 1])
: parseIfJSON(data.value);
Copy link
Member

Choose a reason for hiding this comment

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

superfluous negation makes it harder to read

Suggested change
const latestValue =
typeof data.values !== 'undefined'
? parseIfJSON(data.values[data.values.length - 1])
: parseIfJSON(data.value);
const latestValue =
typeof data.values === 'undefined'
? parseIfJSON(data.value);
: parseIfJSON(data.values[data.values.length - 1])

ditto below

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 here and below

@samsiegart samsiegart enabled auto-merge (rebase) December 14, 2023 04:12
@samsiegart samsiegart merged commit ebfc261 into main Dec 14, 2023
1 check passed
@samsiegart samsiegart deleted the remove-batch-queries branch December 14, 2023 04:13
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.

2 participants