[ruby-core:121421] [Ruby Feature#21194] How to manage application-level information in Ruby application
From:
"mame (Yusuke Endoh) via ruby-core" <ruby-core@...>
Date:
2025-03-21 05:06:48 UTC
List:
ruby-core #121421
Issue #21194 has been reported by mame (Yusuke Endoh).
----------------------------------------
Feature #21194: How to manage application-level information in Ruby application
https://bugs.ruby-lang.org/issues/21194
* Author: mame (Yusuke Endoh)
* Status: Open
----------------------------------------
## Goal
I want to manage application-level information (e.g., application configuration) while making it easily accessible from the part classes of the application. Additionally, I want to support multiple instances of the application within a single process.
## Current approach 1: Global variables
The simplest way to achieve this is by using global variables.
```ruby
class MyApp
class Part1
def run
using $config[:part1]...
end
end
class Part2
def run
using $config[:part2]...
end
end
def initialize(config)
$config = config
@part1 = Part1.new
@part2 = Part2.new
end
def run
@part1.run
@part2.run
end
end
app1 = MyApp.new({ part1: "aaa", part2: "bbb" })
# app2 = MyApp.new({ part1: "AAA", part2: "BBB" }) # Cannot create this
app1.run
```
This code is simple and clear, but it does not allow creating multiple `MyApp` instances with different configurations.
To achieve that, we would need to create separate process using `fork` or `spawn`.
This limitation remains even if we replace global variables with constants (`MyApp::Config`) or class methods (`MyApp.config`).
## Current approach 2: Passing configuration via `initialize`
A textbook and well-structured approach is to explicitly pass configuration through `initialize`.
```ruby
class MyApp
class Part1
def initialize(config)
@config = config
end
def run
using @config[:part1]...
end
end
class Part2
def initialize(config)
@config = config
end
def run
using @config[:part2]...
end
end
def initialize(config)
@part1 = Part1.new(config)
@part2 = Part2.new(config)
end
def run
@part1.run
@part2.run
end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
```
This approach allows creating multiple MyApp instances with different configurations in a single Ruby process.
However, it has two major drawbacks:
* `config` must be passed explicitly in every `initialize` and `new` call, making the code verbose.
* Both `Part1` and `Part2` instances hold their own `@config` variables, which is redundant -- especially when creating a large number of small instances (e.g., tree nodes).
## Current approach 3: Thread-local storage
Storing configuration in `Thread[:config]` allows multiple application instances without explicit parameter passing.
```ruby
class MyApp
class Part1
def run
using Thread[:config][:part1]...
end
end
class Part2
def run
using Thread[:config][:part2]...
end
end
def initialize(config)
@config = config
@part1 = Part1.new
@part2 = Part2.new
end
def run
Thread[:config] = config
@part1.run
@part2.run
end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
```
This approach is mostly effective but has an issue:
* `Thread[:config] = @config` must be set at the beginning of `MyApp#run`. While this is manageable if there is only one public API, it becomes error-prone when multiple APIs exist.
Note that using `Fiber#[]` instead of `Thread#[]` has the same issue.
## Proposal
Ideally, we want to support multiple application instances while keeping the simplicity of the global variable approach.
To achieve this, I propose introducing a new type variable, such as `$@config`:
* `$@config` belongs to an instance
* When accessing `$@config`, it is looked up not only in `self` but also by traversing the call stack to find the nearest `self` instance that has `$@config`.
With this, the code could be written as follows:
```ruby
class MyApp
class Part1
def run
$@config[:part1] # accesses MyApp's $@config
end
end
class Part2
def run
$@config[:part2]
end
end
def initialize(config)
$@config = config
@part1 = Part1.new
@part2 = Part2.new
end
def run
@part1.run
@part2.run
end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
```
This behaves similarly to dynamically scoped variables but differs in that it is resolved through the `self` instances.
(`Thread.new` is a bit problematic: if you use `Thread.new` in a method of `MyApp::Part1`, you wouldn't have access to `$@config` in it. It might be nice to take over all `$@x` variables.)
## Feedback wanted
Whenever I write a large Ruby application, I encounter this problem.
However, TBH, I am not entirely confident that my proposed solution is the best one.
Do you ever encounter this problem? How do you deal with the problem when you do? Is there a better workaround?
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/