ELLIOTTCABLE / ocaml-ambient-context

Abstraction over thread/continuation/fibre/sequence-local storage mechanisms for implicit communication context-sharing between applications and their transitive dependencies

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ocaml-ambient-context

This OCaml module provides a API that is type- and dependency-abstracted across thread-local storage, Lwt's sequence-associated storage, and Eio's fiber-local storage — all different approaches to "throwing" information "across the stack" at runtime, without modifying interleaving methods/components/dependencies.

As a library author, depending on ambient-context allows you to

  1. abstract some sort of application-provided information into something like "thread-local storage",
  2. while still being compatible with dependants who are calling into you from an asynchronous context (like Lwt or Eio),
  3. without functorizing your interface over (or even depending on!) specifically Lwt or Eio, or preventing non-Lwt/Eio users from consuming your API.

Simply put, ambient-context allows you to communicate with your dependants in situations where you cannot control intermediate dependencies, and cannot modify the API of your own library to accept a new parameter.

Warning

Ambient context like this — effectively, implicit global state — is usually frowned upon for most uses, and with good reason. This module is intended to be used as a last resort, and exclusively for debugging, tracing, and reporting purposes, if at all possible.

Installation and usage ...

The intended usage of this library is in two collaborating components:

  1. that of a "deep in the dependency-tree" library (e.g. foo-deep-lib),
  2. and a top-of-the-dependency-tree (e.g. widget-app).

The former needs to be able to obtain information from the latter, without changing the API presented to intermediate dependencies (e.g. bar-intermediary-lib) — and equivalently, without changing the function-signatures of intermediate wrappers/callers.

... as a top-level application

If a library you depend on (let's pretend it's foo-deep-lib) uses ambient-context, they're effectively deferring an important decision about how their library communicates with you.

This means you must to choose, and configure, a storage-mechanism relevant to the callsite(s) in your own application.

Your choice will vary depending on from where, in your own code, you're calling into a library that uses ambient-context — that is, whether an asynchronous event-loop (such as Lwt or Eio) exists 'above' your calls on the stack. Having determined whether you'll be calling your dependancy (e.g. foo-deep-lib) from such an asynchronous context , you'll then need to install the relevant storage-provider at runtime with an appropriately-placed call to Ambient_context.set_storage_provider.

Example: if you're writing an Lwt-enabled application, and you'll be calling bar-intermediary-lib below the Lwt_main.run event-loop on the stack, you'll need to install the ambient-context-lwt "storage provider" ...

 ; dune-project
  (depends
   (ocaml
    (>= 4.08))
+  ambient-context.unix
+  ambient-context-lwt
   bar-intermediary-lib
   (alcotest :with-test)
   (ocaml-lsp-server :with-dev-setup)
 ; src/dune
 (executable
  (name widget_app)
- (libraries bar-intermediary-lib))
+ (libraries ambient-context.unix ambient-context-lwt bar-intermediary-lib))

... and at runtime, your application will need to dictate the relevant storage backend (TLS, Lwt, or Eio) for a delineated section of the stack — usually, this involves wrapping the invocation of your asynchronous thread-scheduler's runloop — in this example, Lwt_main.run:

(* src/widget_app.ml *)
module Ctx = Ambient_context

let () =
   let sock = create_socket () in
   let serve = create_server sock in

   (* add this line before [Lwt_main.run]: *)
   Ctx.set_storage_provider (Ambient_context_lwt.storage ()) ;
   Lwt_main.run @@ serve ()

Once your application has configured the appropriate runtime context-storage, you'll presumably need to actually use the ambient context in your calls to foo-deep-lib.

To communicate with transitive dependencies, you need an opaque key — these are usually created and exposed by your transitive dependency; see its documentation.

You can provide ambient values to the transitive dependency via calls to Ambient_context.with_binding; which takes that opaque key, the new value you want to set, and then a callback.

Example: In our Lwt-enabled application, assuming foo-deep-lib takes advantage of the ambient context to communicate about a header it wants to add to HTTP requests, we can set that header in our application's top-level context, and it will be available to foo-deep-lib's calls to Curl:

(* src/widget_app.ml *)
module Ctx = Ambient_context

let () =
   let sock = create_socket () in
   let serve = create_server sock in

   Ctx.set_storage_provider (Ambient_context_lwt.storage ()) ;
   Lwt_main.run @@ fun () ->
   Ctx.with_binding Foo_deep.header_context_key "my header value" @@ fun () ->
      (* This empty [bind] may be necessary; see
         {!Ambient_context_lwt.with_binding}. *)
      Lwt.bind (serve ()) (fun () -> ())

Refer to your dependency's documentation for specific instructions on how to provide the ambient context they expect.

Note

The precise semantics of with_binding depend on the chosen storage-backend; refer to your chosen backend's documentation.

... as a library

This library allows you to avoid depending on, or functorizing over, Lwt.t. In the most basic usage, you simply provide a Ambient_context.key, direct your consumers to the above documentation and then anywhere in your API, you can pull the value 'currently' assigned to your key out of the ambient-context.

You need depend only on ambient-context itself, not ambient-context-lwt, or even lwt itself:

 ; dune-project
  (depends
   (ocaml
    (>= 4.08))
+  ambient-context
   (alcotest :with-test)
   (ocaml-lsp-server :with-dev-setup)
 ; lib/dune
 (library
  (name foo-deep-lib)
- (libraries curl pcre)
+ (libraries ambient-context curl pcre))

Use Ambient_context.create_key to create an opaque key for the value, and expose that to your user:

(* lib/foo_deep.ml *)
module Ctx = Ambient_context

let header_context_key : string Ctx.key = Ctx.create_key ()

(* ... *)

Then, anywhere you like, you should be able to obtain the value assigned to Foo_deep.header_context_key by the consuming application up-stack from you:

(* lib/foo_deep.ml *)
module Ctx = Ambient_context

(* ... *)

let http_request ?headers ?body action url =
   let open Curl in
   let headers =
      match Ctx.get Foo_deep.header_context_key with
      | None -> headers
      | Some header ->
         let header = "x-foo-deep: " ^ header in
         Some (header :: Option.default [] headers)
   in
   (* ... *)

Contributing

  1. Create an opam switch and install the dependencies:

    $ opam switch create . ocaml.5.0.0 --deps-only --no-install
    
    # If you have opam >= 2.2
    $ opam install . --deps-only --with-test --with-dev-setup
    
    # ... or with opam < 2.2
    $ opam install . --deps-only --with-test
    $ opam install ocaml-lsp-server ocamlformat
  2. Install pre-commit, and then configure your checkout:

    $ pre-commit install
    pre-commit installed at .git/hooks/pre-commit

License

Copyright © 2023 ELLIOTTCABLE

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

Abstraction over thread/continuation/fibre/sequence-local storage mechanisms for implicit communication context-sharing between applications and their transitive dependencies


Languages

Language:OCaml 99.6%Language:Standard ML 0.4%