Repository navigation
R2 jsrpc bindings - #7913
R2 jsrpc bindings#7913bthwaites wants to merge 13 commits into
Conversation
🛑 Significant concerns — 1 BLOCKING item: on the JSRPC path, Findings: 1 blocking · 1 warning · 2 info · 1 suggestion The decision is advisory: this comment never approves, blocks or requests changes on the pull request. 🔧 Fix with your agentPaste the following into your coding agent to investigate and fix these issues. 📋 Code Quality: 🛑 1 blocking · 1 warning · 2 infoThe multipart RPC Recommendation: 🔴 Serious Concerns
View full reviewScope: This review could not read PR #7092 or its review thread, so it does not check point by point how that feedback was addressed. Everything below comes from this pull request's code.
📝 Documentation: 💬 1 suggestionThe experimental flag has no user-facing documentation. Recommendation: 💬 Optional Nits
View full review
✅ All other checks passed🔒 Security: ✅ LGTM
📖 AGENTS.md: ✅ LGTMNo AGENTS.md concerns. ⚡ Performance: ✅ LGTM
Reviewed commit: |
4e07a46 to
fe2abc0
Compare
|
clanky review with a focus on how comprehensively the feedback on the predecessor PR (#7092) has been addressed |
Unlike the HTTP binding implementation, it is not the JSRPC client's responsibility to emit trace spans - that is the JSRPC server's responsibility. |
jp4a50
left a comment
There was a problem hiding this comment.
The RPC methods should forward binding arguments without validating or normalizing them on the client (marshalling to an appropriate wire representation should be able to be handled by serialize/deserialize methods on the R2 binding API types).
The only exceptions I'm aware of are:
- Adapting oversized
put/uploadPartbodies for JSRPC’s message-size limit - Applying caller-specific compatibility behavior, including
r2_list_honor_includeas described in the previous review (#7092 (comment)) - (Possibly) handling upload-stream cancellation - it's worth testing this to see what happens if, for example, the JSRPC server rejects the RPC without reading the body or after partially reading the body, then see if any changes (e.g. cancellation guard) are required to achieve the desired behaviour.
| KJ_UNREACHABLE; | ||
| } | ||
|
|
||
| /***************************** |
There was a problem hiding this comment.
[nit] Remove comment. Doesn't add meaningful value.
| } | ||
| KJ_IF_SOME(requestedIncludes, o.include) { | ||
| for (auto& requested: requestedIncludes) { | ||
| JSG_REQUIRE(requested.value == "httpMetadata" || requested.value == "customMetadata", |
There was a problem hiding this comment.
This comment from the previous PR is still unaddressed. We need all client-side input validation to go.
The JSRPC API contract is that all binding calls from the user must reach the server (assuming the server at least holds up its side of the contract/exposes the correct API methods etc), even if their input is malformed
| KJ_CASE_ONEOF(value, Range) { | ||
| kj::Vector<jsg::Dict<double>::Field> fields; | ||
| KJ_IF_SOME(offset, value.offset) { | ||
| JSG_REQUIRE(offset >= 0, RangeError, "Invalid range. Starting offset (", offset, |
There was a problem hiding this comment.
Remove input validation.
In general, all of the preamble before the call to callR2RpcMethod should be able to be removed. JSG_STRUCTs are generally JSG-serializable by default so don't need to be transformed before being passed to JSRPC as long as their fields are also JSG-serializable (and I believe Carol's now made all non-trivially-serializable types like Checksums serializable now (via JSG_SERIALIZABLE).
LMK if there is any reason any of this preamble can't be removed.
| return kj::mv(checksum); | ||
| }; | ||
|
|
||
| jsg::Optional<PutOptions> rpcOptions; |
There was a problem hiding this comment.
Remove all input validation and argument (PutOptions) transformation before RPC call - should no longer be needed.
| kj::String, kj::Maybe<R2PutValue>, jsg::Optional<PutOptions>)>>& putFnHandler, | ||
| const jsg::TypeHandler<jsg::Promise<kj::Maybe<HeadBackendResult>>>& putResultHandler) { | ||
| return js.evalNow([&] { | ||
| auto cancelReader = kj::defer([&] { |
There was a problem hiding this comment.
This cancellation guard seems to be dropped before the RPC call is made so once input validation is removed it will become redundant.
| }; | ||
|
|
||
| R2MultipartUpload(kj::String key, kj::String uploadId, jsg::Ref<R2Bucket> bucket) | ||
| // The gateway's completion response omits create-time metadata. Created RPC uploads retain |
There was a problem hiding this comment.
This comment has not been addressed from the predecessor PR. Please address or respond to all comments from the previous PR.
There was a problem hiding this comment.
Yes, I missed that you added another round of comments on Oct 21. Doing another pass through for those.
| // as the key) by mistake. NonCoercible so that a non-string uploadId is rejected rather than | ||
| // stringified. A Dict rather than a JSG_STRUCT because structs must be registered in the | ||
| // isolate's type list, which is at the compiler's fold-expression nesting limit. | ||
| using CreateMultipartUploadBackendResult = jsg::Dict<jsg::NonCoercible<kj::String>>; |
There was a problem hiding this comment.
I discussed with @Carolx715 that this should be made it into a JSG_STRUCT. You should be able to work around the compiler limitation by updating -fbracket-depth in the compile options (grep for it as it's set in multiple places in the codebase) from 512 to e.g. 1024.
| return uploadPromise.then(js, | ||
| [bucket = JSG_THIS, key = kj::mv(key), metadata = kj::mv(metadata)]( | ||
| jsg::Lock& js, CreateMultipartUploadBackendResult result) mutable { | ||
| KJ_ASSERT(result.fields.size() == 1 && result.fields[0].name == "uploadId"_kj, |
There was a problem hiding this comment.
This assertion seems unnecessarily restrictive. I think assertions on JSRPC call return values should be limited only to those that would otherwise prevent a valid local result from being constructed.
| uploadPartFnHandler, | ||
| const jsg::TypeHandler<jsg::Promise<UploadedPart>>& uploadPartResultHandler) { | ||
| return js.evalNow([&] { | ||
| JSG_REQUIRE(partNumber >= 1 && partNumber <= 10000, TypeError, |
There was a problem hiding this comment.
Remove all input validation. Leave this to the server.
| const jsg::TypeHandler<jsg::Promise<R2Bucket::HeadBackendResult>>& completeResultHandler) { | ||
| return js.evalNow([&] { | ||
| for (auto& part: uploadedParts) { | ||
| JSG_REQUIRE(part.partNumber >= 1 && part.partNumber <= 10000, TypeError, |
@bthwaites could you add some test coverage for this. It may be that no cancellation guard or other code changes are required to make them pass but I think testing backend rejection before the body is read or after it's only partially read would be worthwhile. |
jp4a50
left a comment
There was a problem hiding this comment.
Nice!
Thanks @Carolx715 and @bthwaites . It's been a journey. Appreciate the patience shown to work towards a nice clean API.
As per internal chat, we could still do with explicit test coverage clarifying the behaviour when:
- JSRPC server explicitly cancels stream (codifies the cancellation behaviour we get with the gate either on or off)
- JSRPC server throws before (or during?) consumption of the stream
But I don't think that needs to block merging this PR.
| class R2MultipartUpload: public jsg::Object { | ||
| public: | ||
| struct UploadedPart { | ||
| int partNumber; |
There was a problem hiding this comment.
I set an agent on the task of assessing how transparently the latest changes forward the arguments passed to the R2 binding API calls up to the JSRPC server.
One quirk is that a couple user-facing fields (UploadedPart.partNumber and ListOptions.limit) are implicitly narrowed from a JS number to an int when they enter C++, meaning that if someone sets them to a non-integer they will get rounded (e.g. caller passes 1.5, JSRPC server sees 1).
I don't think that's a massive deal but it's a quirky little exception to the API contract of "pass all arguments to the JSRPC server without validation or transformation".
Not blocking for this PR but perhaps we should think about either documenting this or fixing it in a follow up.
| # overriden in config=opt | ||
| build --@capnp-cpp//src/kj:debug_memory=True | ||
| build --cxxopt="-fbracket-depth=512" --host_cxxopt="-fbracket-depth=512" | ||
| build --cxxopt="-fbracket-depth=1024" --host_cxxopt="-fbracket-depth=1024" |
There was a problem hiding this comment.
[nit] Could you also update -fbracket-depth=512 to 1024 in compile_flags.txt and doxyfile?
|
Added the stream cancellation test cases and updated fbracket-depth to 1024 |
The JSRPC methods on R2Bucket and R2MultipartUpload no longer create R2 user trace spans or set request/response tags. The HTTP-backed methods keep their spans unchanged. The JSRPC spans could not report cloudflare.r2.response.success or the R2 error tags, because the gateway's RPC results do not carry the backend status. Rather than emit spans that diverge from the HTTP transport, drop them until the RPC protocol can support parity. Drop addHttpMetadataRequestSpanTags and the TraceContext parameter of makeSerializableConditional, which only the JSRPC methods used. r2-jsrpc-test no longer attaches the instrumentation tail worker, and r2-instrumentation-test compares the HTTP transport's spans exactly.
- Preserve list.include compatibility - Replace multipart result dict with JSG struct - TS updates - Split tests
…ancel-propagation
c75b57c to
fcbdd08
Compare
| // reenable the type | ||
| // Keep the existing generic error behavior for R2 bindings. Changing this throw path to | ||
| // throw a structured R2Error would be a public compatibility change, and R2Error would also | ||
| // need to be made serializable so it can cross JSRPC. |
There was a problem hiding this comment.
Nit: Maybe we should revert this comment since it's slightly outdated now? It implies that we decided to keep it as is going forward. I added the comment initially to elaborate on how R2 users expect a plain error string to be returned and we should keep that behavior for back compat. But that depends on whether or not we ever plan on supporting throwing R2Error structs, which is a conversation for a later time and not really relevant to this PR
There was a problem hiding this comment.
INFO: Moving a discussion from DMs to here - Since the contract isn't fully settled yet & this is only used for testing right now we can keep the contract in workerd/api/tests/r2-binding-service for lack of a better place. On public release we probably want to move this to a different directory
Picking up from @Carolx715 's PR: #7092