We’re planning to add a stdlib API for fiber-local storage, which is a critical component for concurrent applications.
With the need for such a feature and the absence of an official API, Crystal libraries have adopted several custom implementations in order to get things working. But they’re often subpar (in general; they might be fine for the specific use case).
The goal is to implement a solid and efficient standard API to replace all currently used makeshift alternatives.
In order to understand how Crystal libraries use fiber local storage, I digged through publicly available sources and I’m going to list my findings here. It’s actually quite a large amount of shards using some form of fiber-local storage.
I’ve looked for some specific patterns which are commonly associated with fiber-local storage implementations. It’s likely that I missed some more unique implementations. Please add missing use cases in a comment!
I appreciate any comments on the exploration and classification. Do you think it makes sense?
This study is intended to inform an RFC about fiber-local storage API for Crystal’s standard library.
Implementation patterns
All implementations I found use one of two base mechanism. Of course, each individual implementation may add different amounts of convenience wrapper around.
Custom property on Fiber
class Fiber
property my_fls : String?
end
Fiber.current.my_fls = "foo"
- Monkey patching additional properties into
Fiber
is not ideal. There’s no direct danger besides name clashes with other libraries which do the same. But it’s also not a clean solution. - This is equivalent to a class variable, so there’s no direct
M:N
mapping.
List of implementations
- Log: Observability context
Fiber#@logging_context
- Object: Recursion detection
Fiber#@exec_recursive_hash
,Fiber#@exec_recursive_clone_hash
. Is a hash for M:N mapping. - Avram: Caching avram/src/avram/charms/fiber.cr at 374559f79f12014a92980f730471e346bd4065d5 · luckyframework/avram · GitHub
- Datadog: Observability context datadog/src/datadog.cr at 9808f7ceb663f4b87f321eb909469f46e5bf1663 · jgaskins/datadog · GitHub
- Honeybadger: Observability (Error Monitoring - Honeybadger Documentation, similar to Log::Context)
honeybadger-crystal/src/core_ext/fiber.cr at d50b2c0858425326d97a012651a8e7b0ab5ef541 · honeybadger-io/honeybadger-crystal · GitHub - Rosetta: I18n configuration rosetta/src/rosetta/ext/fiber.cr at 4b6bf83483863de04c6f36cf4f20969833a45f13 · wout/rosetta · GitHub
- Athena: Container for dependency injection. Task-scoped.
athena/src/components/dependency_injection/src/athena-dependency_injection.cr at ebdb367aee095fbd0427fe6a33b57d39e2f86afe · athena-framework/athena · GitHub - Mollie: Client configuration (seems like a bit odd; this looks like it should rather be passed explicitly) mollie.cr/src/mollie/ext/fiber.cr at 8bd991229696653fea93145cbb83da6bd36abc02 · wout/mollie.cr · GitHub
- Opentelemetry SDK: Observability context
opentelemetry-sdk.cr/src/ext/fiber.cr at c7237732135f6d0cab84abc6bb6d438c9f8f9d66 · wyhaines/opentelemetry-sdk.cr · GitHub - Opentelemetry: Observability context opentelemetry/src/ext/fiber.cr at d47d21314a33f5b51360ad0749c8159c7dfe3b85 · jgaskins/opentelemetry · GitHub
- Telegram bot: Client configuration (seems like a bit odd; this looks like it should rather be passed explicitly) telegram_bot/src/telegram_bot/fiber.cr at 39e0914d52925b57636bc32420961a308ed01b8b · hangyas/telegram_bot · GitHub
- Sidekiq: Observability context
sidekiq.cr/src/sidekiq/logger.cr at 0e46bd8ce16a5d6ab1279c2522f2d19d4d202448 · hugopl/sidekiq.cr · GitHub - Breeze: ? breeze/src/charms/fiber.cr at f1e52df029f79a201da96032d53e383a31c0ab18 · luckyframework/breeze · GitHub
- Money: Lib configuration (rounding mode, default currency, …)
money/src/ext/fiber.cr at d63f65143f3a9fdc9a0df2d6bff58e69681383b6 · crystal-money/money · GitHub
Hash mapping Fiber
instances to values
fibers = Hash(Fiber, String).new.compare_by_identity
MyFLS.fibers[Fiber.current] = "foo"
- The hash can be a class or instance property. The latter allows
M:N
mappings which is necessary for some use cases (such as connection pools). - Some hashes use
Fiber#object_id
instead of theFiber
instance as the key. I’m not sure what’s the point of that. Maybe this predatesHash#compare_by_identity
?
List of implementations
Hash lookup based on Fiber#object_id
:
- Marten: DB transactions.
Marten::DB:Connection::Base#@transactions
marten/src/marten/db/connection/base.cr at 80bf8de0006711444a2e2fcd29137860a0ef1824 · martenframework/marten · GitHub - Jennifer: DB transactions.
Jennifer::Adapter::Transactions#@locks
jennifer.cr/src/jennifer/adapter/transactions.cr at 0f4e4bd58112dafc92ca0d76814001a66a50d745 · imdrasil/jennifer.cr · GitHub - Cr-i18n: Locale/language configuration.
Cr18n::Labels.@contexts
cr-i18n/src/cr-i18n/localization.cr at 332dff37139d1193171d66bd13960c47371cc418 · crystal-community/cr-i18n · GitHub - Ohm: Connection pool.
WeakPool(T)#@pool
ohm-crystal/src/ohm.cr at 0d60236545ec77073a0378e4f76759389046e343 · soveran/ohm-crystal · GitHub
Hash lookup based on Fiber
- myecs: Recursion detection.
ECS::WORLD_BEING_LOADED
myecs/src/myecs.cr at cdfd7005589a8de0bfdc27778dfd5ff2efe7acc3 · konovod/myecs · GitHub - Notifications: Per-fiber timing of execution, potentially nested.
Timed.@@timestack
crystal-notifications/src/notifications/fanout.cr at 93a72b9c92372c948baa655fd7e118b16abbc38b · crystal-community/crystal-notifications · GitHub - Pool: Pool checkout.
ConnectionPool(T)#@connections
pool/src/connection.cr at de1e315dc2349cb47303c91bd0d6c5f3446629dc · ysbaddaden/pool · GitHub
Extra: Thread Local Storage
Stdlib also has some use cases with thread local storage. This is insufficient when fibers can switch threads (`ThreadLocalValue` is not fiber-aware · Issue #15088 · crystal-lang/crystal · GitHub).
This affects Regex::PCRE2#@match_data
and Regex::PCRE2.@@jit_stack
.
Use case classification
The different use cases can be classified into several categories:
- Observability tools
- Pool checkouts and transactions
- Default configuration like math rounding mode (Add `Number.rounding_mode` + `.{with,save}_rounding_mode` by Sija · Pull Request #11097 · crystal-lang/crystal · GitHub, Default rounding mode for primitive floating-point operations · Issue #15192 · crystal-lang/crystal · GitHub) or locale configuration
- Timeouts, deadlines, cancellation (Go’s context package, see also forum.crystal-lang.org/t/how-do-you-manage-context/5572)
I think there are two different modes of how fiber local variables are used:
- A fixed property assigned to a fiber. It has exactly one value per fiber (or none through lazy initialization) and that value is unique per fiber. It must not be inherited by child fibers (structured concurrency).
Example: Pool checkouts, transactions, recursion detection - A scoped property which may change over time. Assignments are usually scoped to a specific range of execution, and may be inherited by child fibers (structured concurrency)
Example: observability contexts, configuration
A challenge for the stdlib API is how to suit the needs for both of these modes.
Non use-cases
It’s also important to clarify what are not appropriate use case for fiber-local storage.
I think it is primarily intended for matters of code structure (such as a DB transaction is checked out to one specific strain of execution, i.e. a fiber).
Fiber local storage is not suitable for domain specific values, such as client sessions in a web server. These should rather be passed explicitly to clearly express the contextual flow.
Examples for such questionable use are mollie.cr/src/mollie/ext/fiber.cr at 8bd991229696653fea93145cbb83da6bd36abc02 · wout/mollie.cr · GitHub and telegram_bot/src/telegram_bot/fiber.cr at 39e0914d52925b57636bc32420961a308ed01b8b · hangyas/telegram_bot · GitHub