2

I've created a reactive flow at my controller Endpoint addEntry where one object inside should be created only once per request since it holds a state.

@Override
public Mono<FileResultDto> addEntry(final Flux<byte[]> body,
                                    final String fileId) {
    return keyVaultRepository.findByFiletId(fileId)
            .switchIfEmpty(Mono.defer(() -> {
                final KeyVault keyVault = KeyVault.of(fileId);
                return keyVaultRepository.save(keyVault);
            }))
            .map(keyVault -> Mono
                    .just(encryption.createEncryption(keyVault.getKey(), ENCRYPT_MODE)) // createEncryption obj. that holds a state
                    .cache())
            .map(encryption -> Flux
                    .from(body)
                    .map(bytes -> encryption
                            .share()
                            .block()
                            .update(bytes) // works with the state and changes it per byte[] going through this flux
                    )
            )
            .flatMap(flux -> persistenceService.addEntry(flux, fileId));
}

before I asked this question I used

encryption.block() which was failing.

I found this one and updated my code accordingly (added .share()).

The test itself is working. But I am wondering if this is the proper way to go to work with an object that should be created and used only once in the reactive flow, provided by

encryptionService.createEncryption(keyVault.getKey(), ENCRYPT_MODE)

Happy to hear your opinion

Anna Klein
  • 1,906
  • 4
  • 27
  • 56

1 Answers1

0

Mono.just is only a wrapper around a pre-computed value, so there is no need to cache or share it, because it is only just giving back a cached variable on subscription.

But, in your example, there is something I do not understand.

If we simplify / decompose it, it gives the following:

Mono<KeyVault> vault = keyVaultRepository.findByFiletId(fileId)
            .switchIfEmpty(Mono.defer(() -> keyVaultRepository.save(KeyVault.of(fileId));
            ));

Mono<Mono<Encryption>> fileEncryption = vault
        .map(it -> Mono.just(createEncryption(it.getKey)).cache()); // <1>

Mono<Flux<Encryption>> encryptedContent = fileEncryption.map(encryption -> Flux
                    .from(body)
                    .map(bytes -> encryption
                            .share()
                            .block()
                            .update(bytes))); // <2>

Mono<FileResultDto> file = encryptedContent.map(flux -> persistenceService.addEntry(flux, fileId));
  1. Why are you trying to wrap your encryption object ? The result is already part of a reactive pipeline. Doing Mono.just() is redundant because you are already in a map operation, and doing cache() over just() is also redundant, because a "Mono.just" is essentially a permanent cache.
  2. What does your "update(bytes)" method do ? Does it mutate the same object every time ? because if it does, you might have a problem here. Reactive streams cannot ensure thread-safety and proper ordering of actions on internal mutated states, that is out of its reach. You might bypass the problem by using scan operator, though.

Without additional details, I would start refactoring the code like this:

Mono<KeyVault> vault = keyVaultRepository.findByFileId(fileId)
      .switchIfEmpty(Mono.defer(() -> keyVaultRepository.save(KeyVault.of(fileId));

Mono<Encryption> = vault.map(it -> createEncryption(it.getKey()));

Flux<Encryption> encryptedContent = fileEncryption
    .flatMapMany(encryption -> body.scan(encryption, (it, block) -> it.update(block)));

Mono<FileResultDto> result = persistenceService.addEntry(encryptedContent, fileId);
amanin
  • 3,436
  • 13
  • 17
  • Thanks for the reply first. Do I understand it correctly that when the body byte[] stream will be consumed, the chain of Monos before will only emmit one single Encryption object ONCE which will be used for every byte[] emitted byte the Flux(this is what I want)? I thought that whenever the consumer sends an onNext signal upstream to request and handle more byte[] that everytime a new Encryption object will be created (like it would be with java stream api intermediate operations). To answer 2: update encrypts the bytes passed (internally uses java cipher) – Anna Klein Dec 06 '22 at 10:09
  • 1
    It depends on how you chain your publishers (Mono/Flux). If you look at my example, I subscribe only once on the 'fileEncryption' Mono, because it triggers the byte[] Flux *after* it emits it next (and only) item. If we inverse the dependency, and consume the encryption Mono from each received byte[] (for example by doing: `body.concatMap(block -> fileEncryption.map(encryption -> encryption.update(block)))`, then the encryption mono would be subscribed and re-executed for each element of the flux. – amanin Dec 06 '22 at 12:14