-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add the ClientEncryption.createEncryptedCollection
helper method
#1079
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
Conversation
driver-core/src/main/com/mongodb/client/model/CreateCollectionOptions.java
Outdated
Show resolved
Hide resolved
@@ -183,6 +198,85 @@ public Publisher<RewrapManyDataKeyResult> rewrapManyDataKey(final Bson filter, f | |||
})); | |||
} | |||
|
|||
@Override | |||
public Publisher<BsonDocument> createEncryptedCollection(final MongoDatabase database, final String collectionName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rozza, your knowledge of the Project Reactor and the reactive streams programming is being needed when reviewing this method.
The intended behavior of the implementation of this method can be peeked either in the synchronous implementation, or in the spec: Create Encrypted Collection Helper.
The thing that made this implementation really difficult for me (besides the lack of knowledge) is:
- if either
createDataKey
orcreateCollection
fails, the method must attach to theonError
signal the (possibly partially) updatedencryptedFields
that it would have produced in case of a success.
I achieved the needed behavior by capturing dataKeyMayBeCreated
and actualEncryptedFields
in the returned Publisher
. As far as I understand, the Publisher
is constructed such that there are no data races when updating the actualEncryptedFields
document (updates are done to the unrelated field
documents, which are different elements of a BSON array), and all reads of the updated actualEncryptedFields
are ordered after (in the HB order) the updates due to the combination of then
and defer
methods.
Even if this implementation is correct (at the very least I doubt it is idiomatic), it can't work if there are more than one Subscriber
(regardless of whether they exist concurrently with each other or not), which is why I had make the result a one-shot publisher.
.map(dataKey -> new SimpleImmutableEntry<>(field, dataKey)) | ||
) | ||
.iterator(); | ||
Flux<SimpleImmutableEntry<BsonDocument, BsonBinary>> publisherOfFieldsAndDataKeyIds = Flux.concat(publishersOfFieldsAndDataKeyIds) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a result of Flux.concat
all data keys are created sequentially. This way we stop as soon as there is an error, instead possibly continuing to invoke createDataKey
despite there already being an error.
.doOnError(RuntimeException.class, e -> { | ||
if (dataKeyMayBeCreated.get()) { | ||
throw new MongoUpdatedEncryptedFieldsException(actualEncryptedFields, format("Failed to create %s.", namespace), e); | ||
} else { | ||
throw e; | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exceptions from doOnError
/defer
are handled by the Project Reactor such that the reactive streams protocol is not violated. I am not sure what is a better / more idiomatic way of transforming the error.
P.S. While writing this comment, I tried to find a better way again and found Flux.handle
, but it can't help because it is for handling emitted items, not errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that doOnError
suppresses the original e
when we throw MongoUpdatedEncryptedFieldsException
, instead of treating the thrown exception as a replacement for the original one. As a result, we have e
as both the cause of MongoUpdatedEncryptedFieldsException
and an element in the suppressed set, which causes ugly stack-traces and implies that we are abusing doOnError
. However, it may be that the Reactor simply does not support what we need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rozza Thank you for pointing out the onErrorMap
method, the emitted exceptions are nice now. There is no excuse for how I failed to find it myself.
if (!fields.isArray()) { | ||
throw new MongoConfigurationException(format("`encryptedFields` is incorrectly configured for the collection %s." | ||
+ " `encryptedFields.fields` must be an array, but is %s type.", namespace, fields.getBsonType())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This exception is not according to the current spec, and comes from the following:
- https://mongodb.slack.com/archives/C0406ECL478/p1675269046945409
- https://jira.mongodb.org/browse/DRIVERS-2540 (does not have a completed spec change), which proposes to throw in the
createCollection
methods. And I think that ifcreateCollection
methods need to throw, so should thecreateEncryptedCollection
method.
If there is a concern that we should not throw this exception until (if ever) it is approved as a spec change, I will undo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I undone this part because I realized that the proposal in DRIVERS-2540 is not fully-baked.
return OneShotPublisher.from(Mono.defer(() -> { | ||
MongoNamespace namespace = new MongoNamespace(database.getName(), collectionName); | ||
Bson rawEncryptedFields = createCollectionOptions.getEncryptedFields(); | ||
if (rawEncryptedFields == null) { | ||
throw new MongoConfigurationException(format("`encryptedFields` is not configured for the collection %s.", namespace)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of throwing here, it is likely more idiomatic to return Mono.error
, given that we are in the Supplier<Mono>
that is passed to Mono.defer
. However, if we are to follow this approach consistently, the whole Supplier<Mono>
passed to Mono.defer
should be wrapped in a try
statement that catches all non-fatal exceptions and returns Mono.error
instead of throwing them.
But I see no reason to do that:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do this work outside of the construction of the publisher. That way it follows the general validation eg notNull
checks and reduces the code in the callable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially had it like that, but then noticed that the other MongoConfigurationException
we throw if fields
is not an array, should be signalled via the publisher: we need to encode the encryptedFields
config to BsonDocument
, and this does not seem anymore like just an argument check.
Reporting MongoConfigurationException
from the same method in two different ways (throw
or signal onError)
seems weird, so I decided to also signal the first MongoConfigurationException
that you commented on. Given this consideration, does the code seem better now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on your experimental code here, I infer that you still think that failures to render Bson
should be treated as fatal and not signalled via the returned publisher. So I did this work outside of the construction of the publisher as you originally requested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a few comments.
I think there may be a more "functional" way with the reactive streams approach. But I liked the procedural nature of it and how it follows the sync version 👍
if (!fields.isArray()) { | ||
throw new MongoConfigurationException(format("`encryptedFields` is incorrectly configured for the collection %s." | ||
+ " `encryptedFields.fields` must be an array, but is %s type.", namespace, fields.getBsonType())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack
driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java
Outdated
Show resolved
Hide resolved
driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java
Show resolved
Hide resolved
driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java
Outdated
Show resolved
Hide resolved
driver-core/src/main/com/mongodb/client/model/CreateCollectionOptions.java
Outdated
Show resolved
Hide resolved
driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java
Outdated
Show resolved
Hide resolved
return OneShotPublisher.from(Mono.defer(() -> { | ||
MongoNamespace namespace = new MongoNamespace(database.getName(), collectionName); | ||
Bson rawEncryptedFields = createCollectionOptions.getEncryptedFields(); | ||
if (rawEncryptedFields == null) { | ||
throw new MongoConfigurationException(format("`encryptedFields` is not configured for the collection %s.", namespace)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do this work outside of the construction of the publisher. That way it follows the general validation eg notNull
checks and reduces the code in the callable.
driver-core/src/main/com/mongodb/client/model/CreateCollectionOptions.java
Outdated
Show resolved
Hide resolved
driver-core/src/main/com/mongodb/client/model/CreateEncryptedCollectionParams.java
Outdated
Show resolved
Hide resolved
dataKeyOptions.masterKey(masterKey); | ||
} | ||
String keyIdBsonKey = "keyId"; | ||
// any mutable non-thread-safe Boolean should do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand this comment. What is the non-thread-safe boolean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. Do you find the updated text clearer?
driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java
Outdated
Show resolved
Hide resolved
driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java
Outdated
Show resolved
Hide resolved
JAVA-4679
What is that approach? I am most of all interested in how to attach the data key IDs to |
JAVA-4679
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
Me and my big mouth - turned out to be a fun exercise! Here is my attempt of a more functional approach:
|
...ync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionAutoDataKeysTest.java
Show resolved
Hide resolved
@rozza Thanks! I had to read-up about
I also experimented just in case, and as expected, the context entry with the I additionally have doubts about this code: ...
.collectList()
.map(fieldsArray -> encryptedFields.clone().append("fields", new BsonArray(fieldsArray)))
.flatMap(actualEncryptedFields -> ... My understanding is that the function provided to However, I realized that the publisher indeed does not have to be one-shot because the mutable state it captures is created once per I took your idea to use |
- treat errors while encoding `rawEncryptedFields` as fatal, i.e., do that outside of the publisher pipeline; - remove the one-shot publisher as it's not needed; - simplify the `doOnError` call; - undo the change proposed by DRIVERS-2540. JAVA-4679
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
I had a couple of thoughts regarding your comments - but they don't require any actioning:
Context can't be used to propagate information downstream because it only propagates information upstream
According to the context.write docs:
The Context is propagated from the bottom of the chain towards the top.
So I read that as in order of the chain (operations), the bottom being the publishersOfUpdatedFields
in your code and the top being the final Mono.
This makes sense to me as, I used the context in order to get the invalidKeyId
test to pass which was failing when not using the context.
My understanding is that the function provided to map is called only if collectList() emits a list, which happens only if there were no errors. On top of that, collectList is documented as "discards the elements in the List upon cancellation or error triggered by a data signal."
One to check, if the fields list is empty, then collectList
will either emit an empty list or just complete. The map
stage becomes a noop either way and will complete without any changes.
The impact of an error or cancellation signals would be the same for either approach. An error in any stage will bubble up to the top and be caught by any error handlers. I think the same for cancellation signals as well.
@rozza, for the sake of knowledge sharing, I have to disagree:
The following example from the docs: String key = "message";
Mono<String> r = Mono.just("Hello")
.contextWrite(ctx -> ctx.put(key, "World"))
.flatMap( s -> Mono.deferContextual(ctx ->
Mono.just(s + " " + ctx.getOrDefault(key, "Stranger"))));
StepVerifier.create(r)
.expectNext("Hello Stranger")
.verifyComplete(); demonstrates what "from the bottom of the chain towards the top" mean. If it worked according to your interpretation, the emitted string would have been "Hello World", but it is "Hello Stranger".
The
As I mentioned in the PR description, not all needed tests are done in the spec, the spec change in DRIVERS-2538 is still not done. However, we can experiment by temporarily changing the code outside of the
|
Links to the relevant spec changes with their status are gathered and being kept updated in this comment. Note especially that some tests that you may want to see for this change are yet to be described in the spec.
JAVA-4679