promesa

repository·master·Indexed 19 days ago

https://github.com/funcool/promesa

A promise library and concurrency toolkit for Clojure and ClojureScript. It provides syntactic abstractions for working with promises (similar to JS async/await), a CSP implementation for channels as an alternative to core.async, and concurrency patterns such as bulkheads. It leverages JVM virtual threads on JDK 21+ and includes tools for executor management, delayed task scheduling, and parallel sequence processing via px/pmap.

Tokens
7.5K
Snippets
30
Records
34
Agent score
69%

What's inside promesa

  1. How Promesa Channels differ from core.async

    master

    Promesa provides a CSP (Communicating Sequential Processes) implementation for Clojure and ClojureScript that serves as an alternative to core.async.

    Key differences include:

    • No Macro Transformations: The go macro is a simple alias for p/vthread (or p/thread if virtual threads are unavailable). This means you can use standard blocking calls inside go blocks without the limitations of core.async.
    • Virtual Threads (JVM only): On JDK 21+ (or JDK 19 with experimental features), Promesa leverages virtual threads for concurrency.
    • No Callbacks: Functions return promises or block, allowing for standard promise composition or thread blocking.
    • No Take/Put Limits: Channels can handle more than 1024 pending tasks.
    • First-class Errors: Channels support error propagation.
    • Cancellation Behavior: When a channel is closed, Promesa immediately cancels all pending put operations, unlike core.async which allows them to succeed.
    • CLJS Support: While go macros are not available on ClojureScript, all operators (like alts) work using the Promesa API and syntactic abstractions like promesa.core/loop and promesa.core/recur.
  2. How the Bulkhead pattern works in Promesa

    master

    The Bulkhead pattern limits the number of concurrent calls to prevent faults in one part of a system from affecting the entire system.

    Regardless of the implementation type, the bulkhead follows this logic:

    1. Check Concurrency: If the concurrency limit (permits) has not been reached, the function executes immediately.
    2. Queueing: If the limit is reached, the task is queued. In :async mode, it uses an internal queue; in :sync mode, it blocks the current thread.
    3. Rejection: If the queue limit is reached, the task submission is rejected.

    There are two implementation types:

    • :async (or :executor): Runs the task in an executor.
    • :sync (or :semaphore): Runs the task in the current thread (blocking the caller). This implementation works well with JVM virtual threads.
    (require '[promesa.exec.bulkhead :as pxb]
             '[promesa.exec :as px])
    
    ;; Create an async bulkhead with 1 permit and a queue size of 16
    (def instance (pxb/create :type :async :permits 1 :queue 16))
    
    ;; Submit a task to the bulkhead
    @(px/submit instance
                (fn []
                  (Thread/sleep 1000)
                  1))
  3. Optimize performance with thread-first/last macros

    master
    For performance-sensitive code on the JVM, prefer using functions designed for use with ->> (thread-last) macros. These are more optimized because they avoid the automatic unwrapping handling required by functions like then or handle.
  4. Install promesa via deps.edn

    master

    To use promesa in your Clojure or ClojureScript project, add it to your deps.edn file. You can use the Maven central version or pull directly from GitHub using a specific SHA or tag.

    ;; Using Maven central
    funcool/promesa {:mvn/version "12.0.1"}
    
    ;; Using Git
    funcool/promesa
    {:git/sha "7d841fc"
     :git/tag "12.0.1"
     :git/url "https://github.com/funcool/promesa"}
  5. Configure a custom executor for channels

    master

    By default, channels use virtual threads (on JVM) or the common pool for internal dispatching. You can override this by providing a custom executor via the :exc option in the sp/chan constructor.

    (require '[promesa.exec :as px])
    
    (def executor (px/cached-executor))
    
    ;; Create a channel using the custom executor
    (def ch (sp/chan :exc executor))
  6. Basic usage of promesa core

    master

    Promesa provides syntactic abstractions for working with promises, similar to async/await in JavaScript. You can create promises using p/promise and transform them using functional operators like p/map.

    Note: The following example using deref requires a JVM environment, as the JavaScript runtime lacks equivalent blocking primitives.

    (require '[promesa.core :as p])
    
    (->> (p/promise 1)
         (p/map inc)
         (deref))
    ;; => 2
  7. Handle results with `handle` and `finally`

    master

    Use these functions to manage the lifecycle of a promise chain regardless of success or failure.

    • handle: Combines resolved and rejected callbacks into a single function. The callback receives [result error]. If the handler returns a promise, it is automatically unwrapped.
    • finally: Executes a callback regardless of whether the promise was resolved or rejected. The return value of the finally callback is ignored, and a new promise is returned that mirrors the original promise's state.
    ;; handle: [result error]
    (-> (p/promise 1)
        (p/handle (fn [result error]
                    (if error :rejected :resolved))))
    
    ;; finally: always runs
    (-> (p/promise 1)
        (p/finally (fn [_ _]
                    (println "finally"))))
  8. Inspect promise state and values

    master

    A promise exists in one of three states: resolved (contains a value), rejected (contains an error), or pending (no value yet).

    • State checks: Use p/pending?, p/resolved?, and p/rejected? to check the current state.
    • Completion check: Use p/done? to see if the promise is no longer pending.
    • Value access: Use p/extract to get the current value without blocking. Use the @ reader macro (or deref) for blocking access to the value (Note: on CLJS, deref behaves like extract and does not block).

    If p/extract is called on a promise without a value, it returns :no-val if the :no-val keyword is provided.

    (def p1 (p/deferred))
    (def p2 (p/resolved 1))
    (def p3 (p/rejected (ex-info "test" {})))
    
    (p/pending? p1)   ;; => true
    (p/resolved? p2)  ;; => true
    (p/rejected? p3)  ;; => true
    (p/done? p1)     ;; => false
    (p/done? p2)     ;; => true
    
    (p/extract p1 :no-val) ;; => :no-val
    (p/extract p2 :no-val) ;; => 1
    
    ;; Blocking access (JVM only)
    @p2
  9. Add timeouts to async operations with `p/timeout`

    master

    Use p/timeout to prevent an asynchronous task from running indefinitely. If the task takes longer than the specified duration, the promise will be rejected with a timeout error.

    (-> (some-async-task)
        (p/timeout 200)
        (p/then #(println "Task finished" %))
        (p/catch #(println "Timeout" %)))
  10. Using go blocks and go-loop

    master

    In Promesa, go blocks are not macro-transformed code; they are simply executed within a virtual thread (on JVM) or a standard thread.

    Key behaviors:

    • Return Value: Unlike core.async where go returns a channel, Promesa's go returns a promise (CompletableFuture). This allows the block to represent a computation that can fail.
    • go-chan: If you specifically need a go block to return a channel, use the go-chan macro.
    • go-loop: Use go-loop for recursive go blocks (combining go with loop/recur).

    Example:

    (sp/go
      (sp/<! ch 1000 :timeout))
    ;; Returns a promise (CompletableFuture)
    @(sp/go
       (sp/<! ch 1000 :timeout))
  11. Control execution threads using `p/fmap`

    master

    On the JVM, the default execution model for promise chains executes callbacks in the same thread that resolved the previous promise. While efficient for small chains, large or computationally heavy chains can block the thread pool.

    To prevent this, use p/fmap with an executor keyword to schedule callbacks as separate tasks:

    • :default: Uses px/*default-executor* (a ForkJoinPool optimized for small tasks).
    • :vthread: Uses px/*vthread-executor* (Virtual Threads, available on JDK 21+ or JDK 19 with preview enabled).
    (require '[promesa.exec :as px])
    
    @(->> (p/delay 100 1)
          (p/fmap :default inc)
          (p/fmap :default inc))
    ;; => 3
  12. Channel multiplexing with mult and mult*

    master

    To implement pub/sub or multiple readers for the same data, use multiplexers.

    • sp/mult: Creates a new multiplexer. The returned object implements the channel API, so you can put values into it directly.
    • sp/mult*: Used when you already have an existing channel that you want to multiplex.
    • sp/tap: Attaches a channel to a multiplexer.
    • sp/untap: Removes a channel from a multiplexer.

    Closed channels are automatically removed from the multiplexer.

    (def mx (sp/mult))
    
    (sp/go
      (let [ch (sp/chan)]
        (sp/tap mx ch)
        (println "go 1:" (sp/<! ch))
        (sp/close ch)))
    
    (sp/go
      (let [ch (sp/chan)]
        (sp/tap mx ch)
        (println "go 2:" (sp/<! ch))
        (sp/close ch)))
    
    (sp/>! mx :a)