Aleph Networking Library

repository·master·Indexed 25 days ago

https://github.com/clj-commons/aleph

A high-performance networking library for Clojure that exposes network data as Manifold streams. Aleph provides wrappers for HTTP, WebSockets, TCP, and UDP, and allows direct access to the underlying Netty engine. It is fully Ring-compliant and supports HTTP/2 with both backwards-compatibility and low-level connection modes. Key features include an asynchronous HTTP client modeled after clj-http, multipart request decoding, and customizable internal schedulers for high-volume tasks.

Tokens
3.9K
Snippets
9
Records
19
Agent score
82%

What's inside Aleph

  1. Understand Aleph's HTTP/2 API modes

    master

    Aleph provides two distinct ways to interact with HTTP/2, allowing you to choose between standard Ring compatibility and low-level connection control:

    1. Backwards-compatibility mode: This is the default mode. It uses the standard Ring API where your handler receives a single Ring map (with an InputStream body) and returns a Ring response map. In this mode, all streams have equal priority, and flow control uses a default Netty policy of a 64 kb window that auto-replenishes as bytes are read.

    2. HTTP/2 connection mode: This mode provides a connection-level handler API. It allows you to access the entire connection, all streams within that connection, and all frames within those streams. This is intended for users who need fine-grained control over the HTTP/2 protocol.

  2. Handle WebSocket connections in an HTTP handler

    master

    When an HTTP request contains the appropriate Upgrade headers, you can upgrade the connection to a WebSocket by calling (aleph.http/websocket-connection req).

    This returns a deferred that yields a duplex stream. This single stream represents bidirectional communication: you can receive messages from the client using take! and send messages to the client using put!. WebSocket text messages are emitted as strings, and binary messages are emitted as byte arrays.

    (require '[manifold.stream :as s])
    
    (defn echo-handler [req]
      (let [s @(http/websocket-connection req)]
        (s/connect s s))) ; Echoes all messages back to the client
  3. How WebSockets work in Aleph

    master
    On an HTTP request with proper Upgrade headers, you can call (aleph.http/websocket-connection req) to obtain a deferred that yields a duplex stream. This stream represents bidirectional communication: use take! to receive messages and put! to send them. Text messages are emitted as strings, and binary messages as byte arrays.
  4. Configure HTTP/2 flow control and priorities

    master

    When using Aleph's HTTP/2 capabilities, you can manage resource allocation and flow control through the following mechanisms:

    • Flow Control: Aleph exposes APIs to get and set both stream-level and connection-level window sizes. In the default mode, the window size is 64 kb, but users can adjust the initial window size.
    • Prioritization: Aleph provides APIs to get and set stream priorities and dependencies. Note that Aleph does not automatically act on this information; it is the responsibility of the user/developer to implement logic based on these priorities.
    • Server Push: Aleph does not support HTTP/2 server push.
  5. Manage temporary file cleanup for large multipart requests

    master

    Because resource cleanup happens on a different thread than stream consumption, temporary files might be deleted before you finish processing them (e.g., before you can copy them to a permanent location).

    To avoid this, you have two options:

    1. Increase the memory limit: Ensure all parts fit in memory by increasing the :memory-limit parameter. This is recommended if your workload allows for it (e.g., 8-100 MiB).
    2. Use manual cleanup: If you must use temporary files for large data, pass {:manual-cleanup? true} to decode-request. This changes the return value from a single stream to a vector containing [stream cleanup-fn]. You must call cleanup-fn manually once you are finished with the parts to release the resources.
    ;; Option 1: Increase memory limit
    (mp/decode-request req {:memory-limit (* 16 1024 1024)})
    
    ;; Option 2: Manual cleanup for large files
    (defn multipart-handler [req]
      (let [[s cleanup-fn] (mp/decode-request req {:manual-cleanup? true})]
        (doseq [part (s/stream->seq s)]
          (if (:memory? part)
             (:content part)
             (io/copy (:file part))))
        (cleanup-fn)))
  6. Start an HTTP server with Aleph

    master

    Aleph is fully Ring-compliant and can serve as a drop-in replacement for any Ring-compliant server. You can start a server by passing a handler function to aleph.http/start-server.

    Your handler can return a standard Ring response map, or a Manifold deferred representing an eventual response. If you return a Manifold stream as the :body, Aleph will stream each message from the stream as a chunk, which is useful for Server-Sent Events (SSE).

    (require '[aleph.http :as http])
    
    (defn handler [req]
      {:status 200
       :headers {"content-type" "text/plain"}
       :body "hello!"})
    
    (http/start-server handler {:port 8080})
  7. Implement a TCP server with aleph.tcp

    master

    To create a TCP server, provide a handler function to aleph.tcp/start-server. The handler function must accept two arguments:

    1. A duplex stream (s) representing the connection.
    2. A map (info) containing metadata about the client.

    The stream emits byte-arrays and accepts messages that can be coerced into a binary representation. For advanced byte manipulation, it is recommended to use the byte-streams library.

  8. Replace the default internal scheduler for high-volume scheduling

    master

    Aleph's default scheduler uses ScheduledThreadPoolExecutor, which relies on blocking queues. This is inefficient for high-volume scheduling requests (exceeding 100k/sec). To optimize performance, you can provide a custom scheduler by redefining the manifold.time/*clock* var.

    Your custom scheduler must implement the IClock protocol, which requires two functions:

    1. in: Schedules a task to run once after a specified number of milliseconds.
    2. every: Schedules a task to run repeatedly at a specified interval, starting after an initial delay.

    To apply the new scheduler, use alter-var-root on #'manifold.time/*clock*.

  9. Use TCP servers and clients

    master
    An Aleph TCP server handler takes two arguments: a duplex stream and a map containing client information. The stream emits byte-arrays and accepts any data that can be coerced to binary. A TCP client returns a deferred yielding a duplex stream.
  10. Use the Aleph HTTP client

    master

    The Aleph HTTP client is modeled after clj-http, but every request returns a Manifold deferred. You can use @ to wait for the response or d/chain for asynchronous composition. For HTTP/2 support, you must use a connection pool configured with the desired HTTP versions.

    (require
      '[aleph.http :as http]
      '[manifold.deferred :as d]
      '[clj-commons.byte-streams :as bs])
    
    ;; Synchronous-style usage (blocking)
    (-> @(http/get "https://google.com/")
        :body
        bs/to-string
        prn)
    
    ;; Asynchronous composition
    (d/chain (http/get "https://google.com")
             :body
             bs/to-string
             prn)
    
    ;; HTTP/2 usage with connection pool
    (def conn-pool
      (http/connection-pool {:connection-options {:http-versions [:http2 :http1]}}))
    @(http/get "https://google.com" {:pool conn-pool})
  11. Use Netty HashedWheelTimer as an optimized scheduler

    master

    For high-performance scheduling of approximate I/O timeouts, you can integrate Netty's io.netty.util.HashedWheelTimer. This timer uses JCTools MPSC lockless queues for efficiency.

    To use it, implement the IClock protocol by wrapping the HashedWheelTimer for the in function and using a standard scheduled executor for the every function (to handle periodic tasks).

    (import  '[java.util.concurrent Executors TimeUnit])
    (import  '[io.netty.util HashedWheelTimer TimerTask])
    (import  '[manifold.time IClock])
    (require '[aleph.netty :refer [enumerating-thread-factory]])
    (require '[manifold.time :as mtime])
    
    (def hashed-timer-clock
      (let [timer (HashedWheelTimer.
                   (enumerating-thread-factory "manifold-timeout-scheduler" false)
                   10 TimeUnit/MILLISECONDS 1024)
            periodic-clock (mtime/scheduled-executor->clock
                            (Executors/newSingleThreadScheduledExecutor
                             (enumerating-thread-factory "manifold-periodic-scheduler" false)))]
        (reify IClock
          (in [_ interval f]
            (.newTimeout timer (reify TimerTask (run [_ _] (f)))
                         interval TimeUnit/MILLISECONDS))
          (every [_ delay period f]
            (.every ^IClock periodic-clock delay period f)))))
    
    (alter-var-root #'mtime/*clock* (constantly hashed-timer-clock))