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

$batch #675

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

$batch #675

wants to merge 3 commits into from

Conversation

myarmolinsky
Copy link
Member

@myarmolinsky myarmolinsky commented Jun 28, 2023

MVP: $batch

Connects-to: https://github.com/balena-io/balena-api/pull/4501
Post-MVP: atomicityGroup, if, dependsOn, relative path urls, referencing with $, nextLink, multipart (see: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_MultipartBatchFormat), respond-async
Change-type: major

See: https://balena.zulipchat.com/#narrow/stream/345746-aspect.2Fproduct/topic/Prompt.20When.20Restarting.20Containers

@myarmolinsky myarmolinsky self-assigned this Jun 28, 2023
@myarmolinsky myarmolinsky force-pushed the $batch branch 14 times, most recently from b3386e8 to 051cccc Compare June 30, 2023 15:54
@myarmolinsky myarmolinsky changed the title $batch MVP: $batch Jun 30, 2023
@jellyfish-bot jellyfish-bot changed the title MVP: $batch $batch Jun 30, 2023
@myarmolinsky myarmolinsky force-pushed the $batch branch 13 times, most recently from a77b917 to 4fc5b74 Compare July 3, 2023 15:36
@myarmolinsky myarmolinsky force-pushed the $batch branch 5 times, most recently from 49e0ae4 to 2e2ff45 Compare August 7, 2023 16:03
@myarmolinsky myarmolinsky requested a review from Page- August 7, 2023 16:06
@myarmolinsky myarmolinsky marked this pull request as ready for review August 7, 2023 16:06
@myarmolinsky myarmolinsky requested review from thgreasi and removed request for thgreasi August 8, 2023 11:52
Copy link
Contributor

@fisehara fisehara left a comment

Choose a reason for hiding this comment

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

I would change statusCode and body as it's adding a little noise to this PR but it's fine.
The requestId for a $batch request are a different Id then the pineJs entity IDs.

src/sbvr-api/sbvr-utils.ts Outdated Show resolved Hide resolved
method: string;
url: string;
data?: any;
body?: any;
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the reasoning behind renaming data to body?
This is making it a breaking change, is this neccessary for implementing batch support?

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 was following the spec, which specifies that there is a body but says nothing about data, yet we seem to use data in place of body, so it seems like we just named the property in a way that doesn't abide by the spec (I don't know what our reasoning for this was)

If we want to keep it is data just to avoid a breaking change, I don't know if that sounds like good enough reasoning to avoid following the spec which should presumably be helpful for future changes and understanding as well

Copy link
Collaborator

Choose a reason for hiding this comment

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

This predates the odata json $batch but now that it exists I think it makes sense to match that format for our internal data structure as it should simplify a whole bunch of things

src/sbvr-api/uri-parser.ts Outdated Show resolved Hide resolved
src/sbvr-api/uri-parser.ts Outdated Show resolved Hide resolved
src/sbvr-api/uri-parser.ts Outdated Show resolved Hide resolved
src/sbvr-api/uri-parser.ts Show resolved Hide resolved
src/sbvr-api/uri-parser.ts Outdated Show resolved Hide resolved
src/sbvr-api/uri-parser.ts Outdated Show resolved Hide resolved
src/sbvr-api/sbvr-utils.ts Outdated Show resolved Hide resolved
src/sbvr-api/sbvr-utils.ts Show resolved Hide resolved
Copy link
Contributor

@fisehara fisehara left a comment

Choose a reason for hiding this comment

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

Potentially there needs more test coverage to test the correctnes of delivered data back and forth when using batch requests.

Is there any limit on allowed batch requests in one batch?

test/06-batch.test.ts Outdated Show resolved Hide resolved
test/06-batch.test.ts Outdated Show resolved Hide resolved
test/06-batch.test.ts Outdated Show resolved Hide resolved
);
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm missing test cases for:

  • request id request matches with returned response and Id. eg: mutliple gets for different resources, matching returned resources
  • Expectation of two concurrent requests like 2 POST request sending the same matrix_number?
  • At least some tests for $filter, $expand, ... Potentially a map of test parameters executed as test cases using the same structure [{testSet1}, {testSet2}, ...].foreach(it( run the test))

Copy link
Member Author

@myarmolinsky myarmolinsky Sep 25, 2023

Choose a reason for hiding this comment

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

  • The 'successful request should have responses in its body' test sort of tests for the first bullet but I'll add a separate test for multiple requests
  • The requests should be sequential, so this shouldn't need testing, right?
  • What exactly would this be for? This feature should not change how those work. Tests for $filter, $expand, etc. should live in their own test suite, no?

test/fixtures/06-batch/config.ts Outdated Show resolved Hide resolved
test/fixtures/06-batch/translations/v1/index.ts Outdated Show resolved Hide resolved
src/sbvr-api/sbvr-utils.ts Outdated Show resolved Hide resolved
src/sbvr-api/sbvr-utils.ts Outdated Show resolved Hide resolved
src/sbvr-api/sbvr-utils.ts Outdated Show resolved Hide resolved
}),
),
);
requests = req.body.requests;
Copy link
Contributor

Choose a reason for hiding this comment

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

This will execute all requests in one database transaction. Is this desired or should every request run in it's own database transaction?
The background is, if a database transaction fails the qhole transaction is rolled back, thus all batch requests will be rolled back.

@Page- What do you think, should every request in a batch run in its own db tx?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that is part of the point/idea of implementing $batch @fisehara , to run all requests in a batch on one transaction. Part of the following roadmap item: https://roadmap.balena.io/posts/19/allow-changes-to-variables-to-be-batched-and-submitted-all-at-once

@@ -1273,7 +1376,13 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {

// Parse the OData requests
const results = await mappingFn(requests, async (requestPart) => {
const parsedRequest = await uriParser.parseOData(requestPart);
const parsedRequest = await uriParser.parseOData(
Copy link
Contributor

Choose a reason for hiding this comment

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

If the requests are independent, should they be executed in parallel on the DB or should pinejs limit the requests to be executed sequencially to do some kind of rate limiting?

Copy link
Member Author

@myarmolinsky myarmolinsky Sep 25, 2023

Choose a reason for hiding this comment

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

Requests should be executed sequentially. If a request along the way fails, the batch request should fail. However, there will be no rollback if the requests are not atomic

should pinejs limit the requests to be executed sequencially to do some kind of rate limiting?

I don't know enough about pine and the API to answer this well, your opinion based on what I stated above would be better

@myarmolinsky myarmolinsky force-pushed the $batch branch 6 times, most recently from b4905b0 to 8e20418 Compare September 25, 2023 11:53
Change-type: squash
const runODataRequest = (req: Express.Request, vocabulary: string) => {
if (env.DEBUG) {
api[vocabulary].logger.log('Parsing', req.method, req.url);
}

if (req.url.startsWith(`/${vocabulary}/$batch`)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd be good to store this check somewhere as I've seen equivalent .startsWith checks in a few places

const urls = new Set<string | undefined>(
requests.map((request) => request.url),
);
if (urls.has(undefined)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this also check for null?

throw new BadRequestError('Batch requests cannot contain batch requests');
}
const urlModels = new Set(
Array.from(urls.values()).map((url: string) => url.split('/')[1]),
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can limit the splitting to stop on the first match:

Suggested change
Array.from(urls.values()).map((url: string) => url.split('/')[1]),
Array.from(urls.values()).map((url: string) => url.split('/', 1)[1]),

throw new BadRequestError('Requests of a batch request must have a "url"');
}
const containsBatch =
Array.from(urls).filter((url) => !!url?.includes('/$batch')).length > 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Array.from(urls).filter((url) => !!url?.includes('/$batch')).length > 0;
Array.from(urls).some((url) => !!url?.includes('/$batch'));

if (
request.headers != null &&
request.headers['content-type'] == null &&
(req.headers == null || req.headers['content-type'] == null)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
(req.headers == null || req.headers['content-type'] == null)
req.headers?.['content-type'] == null

const ids = new Set<string>(
requests
.map((request) => request.id)
.filter((id) => typeof id === 'string') as string[],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
.filter((id) => typeof id === 'string') as string[],
.filter((id): id is string => typeof id === 'string'),

'Authorization may only be passed to the main batch request',
);
}
const ids = new Set<string>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

The string should be inferred if a string[] is passed in?

Suggested change
const ids = new Set<string>(
const ids = new Set(

requests.find(
(request) =>
request.headers?.authorization != null ||
request.url?.includes('apikey='),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Technically params for authentication can be customized so this isn't perfect - I'm not sure if it's something we can account for but worth being aware of

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