Skip to content

Releases: pellse/assembler

Assembler v0.7.10

09 Apr 19:11
Compare
Choose a tag to compare

This is a minor release to facilitate chaining of Assembler instances. Below is an example from demo project illustrating Assembler chaining:

@RestController
public class VitalsMonitoringStreamController {

    private final Assembler<HR, Vitals> vitalsAssembler;
    private final Assembler<Vitals, AugmentedVitals> augmentedVitalsAssembler;

    private final HeartRateStreamingService heartRateStreamingService;

    VitalsMonitoringStreamController(
            HeartRateStreamingService heartRateStreamingService,
            DiagnosisAIService diagnosisAIService) {

        vitalsAssembler = assemblerOf(Vitals.class)
                .withCorrelationIdResolver(HR::patientId)
                .withRules(
                        rule(Patient::id, oneToOne(cached(this::getPatients, caffeineCache()))),
                        rule(BP::patientId, oneToMany(BP::id, cachedMany(this::getBPs, caffeineCache()))),
                        Vitals::new)
                .build();

        augmentedVitalsAssembler = assemblerOf(AugmentedVitals.class)
                .withCorrelationIdResolver(Vitals::patient, Patient::id)
                .withRules(
                        rule(Diagnosis::patientId, oneToOne(diagnosisAIService::getDiagnosesFromLLM)),
                        AugmentedVitals::new)
                .build();

        this.heartRateStreamingService = heartRateStreamingService;
    }

    @SubscriptionMapping
    @GetMapping(value = "/vitals/stream", produces = TEXT_EVENT_STREAM_VALUE)
    Flux<AugmentedVitals> vitals() {
        return heartRateStreamingService.stream()
                .window(3)
                .flatMap(vitalsAssembler::assembleStream)
                .flatMap(augmentedVitalsAssembler::assemble);
    }
}

What's Changed

  • Added overloaded correlationIdResolver() methods to resolve ID using indirections
  • Adding assembleStream() method to Assembler interface, to return Flux<Stream<R>> instead of Flux<R> and avoid unnecessary transformations when chaining multiple Assembler instances

v0.7.9

17 Mar 22:14
Compare
Choose a tag to compare

This release dramatically improves the performance and robustness of Assembler's reactive caching mechanism under high contention, significantly reducing the risk of cache miss storms, also known as Cache Stampede.

What's Changed

  • Ability to deactivate the default concurrent cache transformer for one-to-many caching in StreamTableFactoryBuilder
  • Added factory methods to ConcurrentCacheFactory for event notification registration
  • New SerializeCacheFactory delegate to synchronize Cache.computeAll() and prevent cache miss storming (cache stampede)
  • Introducing a new AsyncCacheFactory decorator that provides an asynchronous facade for non-async cache implementations, enabling support for async "computeIfAbsent" semantics in caches that don't natively offer it.
  • Enhance AssemblerBuilder with overloaded build methods for better concurrency configuration of rules aggregation

v0.7.8

11 Mar 04:34
Compare
Choose a tag to compare

What's Changed

  • Big improvements in performance and stability
  • Ability to plug different locking strategies for cache concurrency
  • Event hooks to monitor cache concurrency

Dependencies upgrades

  • Project Reactor 3.7.3
  • Caffeine 3.2.0

Assembler v0.7.7

05 Dec 05:43
Compare
Choose a tag to compare

What's Changed

  • The Auto Caching API was renamed to Stream Table to better reflect what the API does, which is conceptually similar to what a KTable is with Apache Kafka:
Flux<BillingInfo> billingInfoFlux = ... // From e.g. Debezium/Kafka, RabbitMQ, etc.;
Flux<OrderItem> orderItemFlux = ... // From e.g. Debezium/Kafka, RabbitMQ, etc.;

var assembler = assemblerOf(Transaction.class)
  .withCorrelationIdResolver(Customer::customerId)
  .withRules(
    rule(BillingInfo::customerId,
      oneToOne(cached(call(this::getBillingInfo), streamTable(billingInfoFlux)))),
    rule(OrderItem::customerId,
      oneToMany(OrderItem::id, cachedMany(call(this::getAllOrders), streamTable(orderItemFlux)))),
    Transaction::new)
  .build();

var transactionFlux = getCustomers()
  .window(3)
  .flatMapSequential(assembler::assemble);
  • Optimizations in reactive read-write lock implementation and ReactiveGuard high level api
  • Updated Project Reactor to version 3.7.0 to match the release of Spring Boot 3.4.0

Assembler v0.7.6

21 Oct 23:44
Compare
Choose a tag to compare

What's Changed

  • The use of embedded Assembler instances allows for the aggregation of sub-queries defined in rules. This new feature was heavily inspired by this great article from Vlad Mihalcea. It enables the modeling of a complex relationship graph (or, as mentioned in the article, a multi-level hierarchical structure) of disparate data sources (e.g., microservices, relational or non-relational databases, message queues, etc.) without triggering either N+1 queries or a Cartesian product.

    See EmbeddedAssemblerTest.java for an example of how to use this new feature:

Assembler<UserVoteView, UserVote> userVoteAssembler = assemblerOf(UserVote.class)
    .withCorrelationIdResolver(UserVoteView::id)
    .withRules(
        rule(User::id, UserVoteView::userId, oneToOne(call(this::getUsersById))),
        UserVote::new
    )
    .build();

Assembler<PostComment, PostComment> postCommentAssembler = assemblerOf(PostComment.class)
    .withCorrelationIdResolver(PostComment::id)
    .withRules(
        rule(UserVote::commentId, oneToMany(UserVote::id, call(assemble(this::getUserVoteViewsById, userVoteAssembler)))),
        PostComment::new
    )
    .build();

Assembler<PostDetails, Post> postAssembler = assemblerOf(Post.class)
    .withCorrelationIdResolver(PostDetails::id)
    .withRules(
        rule(PostComment::postId, oneToMany(PostComment::id, call(assemble(this::getPostCommentsById, postCommentAssembler)))),
        rule(PostTag::postId, oneToMany(PostTag::id, call(this::getPostTagsById))),
        Post::new
    )
    .build();

// If getPostDetails() is a finite sequence
Flux<Post> posts = postAssembler.assemble(getPostDetails());

// If getPostDetails() is a continuous stream
Flux<Post> posts = getPostDetails()
    .windowTimeout(100, Duration.ofSeconds(5))
    .flatMapSequential(postAssembler::assemble);
  • Big improvement in concurrency management, better fairness introduced in reactive read-write lock implementation
  • Support for configuration of Scheduler backed by virtual thread executor
    • By default, AssemblerBuilder will use Schedulers.boundedScheduler() if reactor.schedulers.defaultBoundedElasticOnVirtualThreads system property is present

Assembler v0.7.5

02 Jul 03:33
4f5c87e
Compare
Choose a tag to compare

What's Changed

This release fixes an issue where there is no direct correlation ID between a top-level entity and a sub-level entity by introducing the concept of an ID join.

For example, before this release, there was no way to express the relationship between e.g. a PostDetails and a User because User doesn't have a postId field like Reply does, as described in #33.

record PostDetails(Long id, Long userId, String content) {
}

record User(Long Id, String username) { // No correlation Id back to PostDetails
}

record Reply(Long id, Long postId, Long userId, String content) {
}

record Post(PostDetails post, User author, List<Reply> replies) {
}

Assembler<PostDetails, Post> assembler = assemblerOf(Post.class)
    .withCorrelationIdResolver(PostDetails::id)
    .withRules(
        rule(XXXXX, oneToOne(call(PostDetails::userId, this::getUsersById))), // What should XXXXX be?
        rule(Reply::postId, oneToMany(Reply::id, call(this::getRepliesById))),
        Post::new)
    .build();

Since 0.7.5, this relationship can now be expressed:

Assembler<PostDetails, Post> assembler = assemblerOf(Post.class)
    .withCorrelationIdResolver(PostDetails::id)
    .withRules(
        rule(User::Id, PostDetails::userId, oneToOne(call(this::getUsersById))), // ID Join
        rule(Reply::postId, oneToMany(Reply::id, call(this::getRepliesById))),
        Post::new)
    .build();

This would be semantically equivalent to the following SQL query if all entities were stored in the same relational database:

SELECT 
    p.id AS post_id,
    p.userId AS post_userId,
    p.content AS post_content,
    u.id AS author_id,
    u.username AS author_username,
    r.id AS reply_id,
    r.postId AS reply_postId,
    r.userId AS reply_userId,
    r.content AS reply_content
FROM 
    PostDetails p
JOIN 
    User u ON p.userId = u.id -- rule(User::Id, PostDetails::userId, ...)
LEFT JOIN 
    Reply r ON p.id = r.postId -- rule(Reply::postId, ...)
WHERE 
    p.id IN (1, 2, 3); -- withCorrelationIdResolver(PostDetails::id)

Assembler v0.7.4

25 Jun 01:15
Compare
Choose a tag to compare

This release brings 2 new features:

import org.springframework.cache.CacheManager;
import static io.github.pellse.assembler.caching.spring.SpringCacheFactory.springCache;
...

@Controller
public class SpO2MonitoringGraphQLController {

  record SpO2Reading(SpO2 spO2, Patient patient, BodyMeasurement bodyMeasurement) {
  }

  private final Assembler<SpO2, SpO2Reading> spO2ReadingAssembler;

  SpO2MonitoringGraphQLController(
      PatientService ps,
      BodyMeasurementService bms,
      CacheManager cacheManager) {

    final var patientCache = cacheManager.getCache("patientCache");
    final var bodyMeasurementCache = cacheManager.getCache("bodyMeasurementCache");

    spO2ReadingAssembler = assemblerOf(SpO2Reading.class)
      .withCorrelationIdResolver(SpO2::patientId)
      .withRules(
        rule(Patient::id, oneToOne(cached(call(SpO2::healthCardNumber, ps::findPatientsByHealthCardNumber), springCache(patientCache)))),
        rule(BodyMeasurement::patientId, oneToOne(cached(call(bms::getBodyMeasurements), springCache(bodyMeasurementCache)))),
        SpO2Reading::new)
      .build();
  }
}
  • Ability to configure read and write non-blocking bounded queues in ConcurrentCache

Assembler v0.7.3

29 May 20:23
Compare
Choose a tag to compare

This release primarily aims to enhance performance. It features the following updates:

  • A redesigned Caching API that generically supports both single values and collections of values as cache entries, providing dual Map/MultiMap semantics.
  • A comprehensive overhaul of the caching concurrency logic, leading to a substantial performance improvement over the previous version.

Assembler v0.7.2

25 Apr 21:51
Compare
Choose a tag to compare

This release introduce asynchronous caching for default cache implementation, and reverts the name of the library back to Assembler (previously CohereFlux)

What's Changed

Full Changelog: v0.7.1...v0.7.2

CohereFlux v0.7.1

07 Jun 21:16
Compare
Choose a tag to compare

This is a big release, with a re-architecture of the framework allowing query functions to have access to the whole entity from the upstream instead of having to rely solely on IDs

What's Changed

  • The framework is now called CohereFlux, the whole API was modified to reflect that change
  • Now passing entities T down the entire processing chain instead of ID
  • Adding RuleMapperSource.call() to invoke a queryFunction with list of IDs instead of Collection of top level entities
  • New BatchRule API
  • Adding factory methods in CaffeineCacheFactory for max size and cache access expiry duration
  • Replaced RC with Flux<R> in CohereFlux interface, type parameters are now CohereFlux<T, R>
  • FetchFunction in CacheFactory now returns an empty Map when no RuleMapperSource is defined