[ruby-core:121421] [Ruby Feature#21194] How to manage application-level information in Ruby application

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/
participants (1)
-
mame (Yusuke Endoh)