Feature #21194
openHow to manage application-level information in Ruby application
Description
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.
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.
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:
-
configmust be passed explicitly in everyinitializeandnewcall, making the code verbose. - Both
Part1andPart2instances hold their own@configvariables, 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.
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] = @configmust be set at the beginning ofMyApp#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:
-
$@configbelongs to an instance - When accessing
$@config, it is looked up not only inselfbut also by traversing the call stack to find the nearestselfinstance that has$@config.
With this, the code could be written as follows:
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?
No data to display