Project

General

Profile

Actions

Feature #21194

open

How to manage application-level information in Ruby application

Added by mame (Yusuke Endoh) 9 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:121421]

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:

  • 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.

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:

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

Actions

Also available in: Atom PDF

Like2