ExamplesBy LevelBy TopicLearning Paths
346 Advanced

346: Runtime Context

Functional Programming

Tutorial

The Problem

Distributed systems attach request IDs, trace spans, and user session tokens to every operation without threading those values through every function call. Thread-local storage solves this: each thread has its own copy of the context variable, invisible to other threads. This enables "ambient context" — logging, metrics, and tracing libraries can read the current request ID without the application code explicitly passing it. Java's ThreadLocal, Python's contextvars.ContextVar, and Go's context.Context (passed explicitly) all address the same problem. Rust uses thread_local! for synchronous code and task-local variables (via tokio::task_local!) for async code.

🎯 Learning Outcomes

  • • Declare thread-local storage with thread_local! and RefCell<Option<T>>
  • • Read and write thread-local values with the .with() closure API
  • • Implement with_context as a scoped setter that restores the previous value
  • • Understand why thread-local values are not automatically propagated to spawned threads
  • • Recognize the difference between thread-local (per OS thread) and task-local (per async task)
  • • Use this pattern for request ID propagation, logging context, and tracing spans
  • Code Example

    #![allow(clippy::all)]
    //! # Runtime Context
    //! Task-local storage and context propagation in async code.
    
    use std::cell::RefCell;
    use std::thread_local;
    
    thread_local! {
        static CONTEXT: RefCell<Option<String>> = RefCell::new(None);
    }
    
    pub fn set_context(ctx: String) {
        CONTEXT.with(|c| *c.borrow_mut() = Some(ctx));
    }
    pub fn get_context() -> Option<String> {
        CONTEXT.with(|c| c.borrow().clone())
    }
    pub fn clear_context() {
        CONTEXT.with(|c| *c.borrow_mut() = None);
    }
    
    pub fn with_context<R>(ctx: String, f: impl FnOnce() -> R) -> R {
        let old = get_context();
        set_context(ctx);
        let result = f();
        match old {
            Some(c) => set_context(c),
            None => clear_context(),
        }
        result
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn context_propagation() {
            set_context("test".into());
            assert_eq!(get_context(), Some("test".into()));
            clear_context();
            assert_eq!(get_context(), None);
        }
        #[test]
        fn with_context_restores() {
            set_context("outer".into());
            let inner = with_context("inner".into(), || get_context());
            assert_eq!(inner, Some("inner".into()));
            assert_eq!(get_context(), Some("outer".into()));
        }
    }

    Key Differences

    AspectRust thread_local!OCaml Domain.DLS
    ScopePer OS threadPer domain (OCaml 5)
    Async task scopeRequires tokio::task_local!Requires effect-based propagation
    InitializationLazy, per threadVia new_key factory
    Interior mutabilityRefCell neededImplicit (mutable by default)
    Cross-thread visibilityNone (by design)None (by design)

    OCaml Approach

    OCaml 5 uses Domain.DLS (Domain-Local Storage) for the equivalent:

    let key = Domain.DLS.new_key (fun () -> None)
    
    let set_context ctx =
      Domain.DLS.set key (Some ctx)
    
    let get_context () =
      Domain.DLS.get key
    
    let with_context ctx f =
      let old = get_context () in
      set_context (Some ctx);
      let result = f () in
      Domain.DLS.set key old;
      result
    

    In OCaml 4, plain ref values suffice since threads share a GIL — each "thread-local" is just a per-thread mutable reference without synchronization concerns. Effect-based frameworks (OCaml 5) can propagate context automatically across continuations.

    Full Source

    #![allow(clippy::all)]
    //! # Runtime Context
    //! Task-local storage and context propagation in async code.
    
    use std::cell::RefCell;
    use std::thread_local;
    
    thread_local! {
        static CONTEXT: RefCell<Option<String>> = RefCell::new(None);
    }
    
    pub fn set_context(ctx: String) {
        CONTEXT.with(|c| *c.borrow_mut() = Some(ctx));
    }
    pub fn get_context() -> Option<String> {
        CONTEXT.with(|c| c.borrow().clone())
    }
    pub fn clear_context() {
        CONTEXT.with(|c| *c.borrow_mut() = None);
    }
    
    pub fn with_context<R>(ctx: String, f: impl FnOnce() -> R) -> R {
        let old = get_context();
        set_context(ctx);
        let result = f();
        match old {
            Some(c) => set_context(c),
            None => clear_context(),
        }
        result
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn context_propagation() {
            set_context("test".into());
            assert_eq!(get_context(), Some("test".into()));
            clear_context();
            assert_eq!(get_context(), None);
        }
        #[test]
        fn with_context_restores() {
            set_context("outer".into());
            let inner = with_context("inner".into(), || get_context());
            assert_eq!(inner, Some("inner".into()));
            assert_eq!(get_context(), Some("outer".into()));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn context_propagation() {
            set_context("test".into());
            assert_eq!(get_context(), Some("test".into()));
            clear_context();
            assert_eq!(get_context(), None);
        }
        #[test]
        fn with_context_restores() {
            set_context("outer".into());
            let inner = with_context("inner".into(), || get_context());
            assert_eq!(inner, Some("inner".into()));
            assert_eq!(get_context(), Some("outer".into()));
        }
    }

    Deep Comparison

    OCaml vs Rust: Runtime Context

    Overview

    See the example.rs and example.ml files for detailed implementations.

    Key Differences

    AspectOCamlRust
    Type systemHindley-MilnerOwnership + traits
    MemoryGCZero-cost abstractions
    MutabilityExplicit refmut keyword
    Error handlingOption/ResultResult<T, E>

    See README.md for detailed comparison.

    Exercises

  • Request ID logger: Implement a logging function log(msg: &str) that prepends the current context (request ID) to every message; demonstrate nested contexts with different IDs.
  • Thread propagation: Spawn a thread inside a with_context block; observe that the spawned thread does NOT inherit the context; manually copy it by capturing the value before spawning.
  • Async task-local: Using tokio::task_local!, implement the same with_context pattern for async tasks; verify that concurrent tasks each see their own context independently.
  • Open Source Repos