From 6aecb58a34283f58f40cfb05e2245c55e0439190 Mon Sep 17 00:00:00 2001 From: Zsolt Kozaroczy Date: Sat, 13 Aug 2022 23:36:50 +0200 Subject: [PATCH] Thread safety (#2520) * Make Config.locale and Config.random thread safe * Make UniqueGenerator thread safe --- lib/faker.rb | 17 ++++---- lib/helpers/unique_generator.rb | 24 ++++++------ .../default/test_faker_unique_generator.rb | 39 +++++++++++++++++++ test/test_determinism.rb | 20 ++++++++++ 4 files changed, 82 insertions(+), 18 deletions(-) diff --git a/lib/faker.rb b/lib/faker.rb index eb0b30f7fe..47f6df1369 100644 --- a/lib/faker.rb +++ b/lib/faker.rb @@ -13,23 +13,26 @@ module Faker module Config - @locale = nil - @random = nil - class << self - attr_writer :locale, :random + def locale=(new_locale) + Thread.current[:faker_config_locale] = new_locale + end def locale # Because I18n.locale defaults to :en, if we don't have :en in our available_locales, errors will happen - @locale || (I18n.available_locales.include?(I18n.locale) ? I18n.locale : I18n.available_locales.first) + Thread.current[:faker_config_locale] || (I18n.available_locales.include?(I18n.locale) ? I18n.locale : I18n.available_locales.first) end def own_locale - @locale + Thread.current[:faker_config_locale] + end + + def random=(new_random) + Thread.current[:faker_config_random] = new_random end def random - @random || Random + Thread.current[:faker_config_random] || Random end end end diff --git a/lib/helpers/unique_generator.rb b/lib/helpers/unique_generator.rb index 21f375d679..b6115671ff 100644 --- a/lib/helpers/unique_generator.rb +++ b/lib/helpers/unique_generator.rb @@ -2,16 +2,9 @@ module Faker class UniqueGenerator - @marked_unique = Set.new # Holds names of generators with unique values - - class << self - attr_reader :marked_unique - end - def initialize(generator, max_retries) @generator = generator @max_retries = max_retries - @previous_results = Hash.new { |hash, key| hash[key] = Set.new } end def method_missing(name, *arguments) @@ -20,9 +13,9 @@ def method_missing(name, *arguments) @max_retries.times do result = @generator.public_send(name, *arguments) - next if @previous_results[[name, arguments]].include?(result) + next if previous_results[[name, arguments]].include?(result) - @previous_results[[name, arguments]] << result + previous_results[[name, arguments]] << result return result end @@ -39,8 +32,17 @@ def respond_to_missing?(method_name, include_private = false) RetryLimitExceeded = Class.new(StandardError) + def previous_results + Thread.current[:faker_unique_generator_previous_results] ||= {} + Thread.current[:faker_unique_generator_previous_results][@generator] ||= Hash.new { |hash, key| hash[key] = Set.new } + end + def clear - @previous_results.clear + previous_results.clear + end + + def self.marked_unique + Thread.current[:faker_unique_generator_marked_unique] ||= Set.new end def self.clear @@ -51,7 +53,7 @@ def self.clear def exclude(name, arguments, values) values ||= [] values.each do |value| - @previous_results[[name, arguments]] << value + previous_results[[name, arguments]] << value end end end diff --git a/test/faker/default/test_faker_unique_generator.rb b/test/faker/default/test_faker_unique_generator.rb index 4f0f7e4765..c87d1319f8 100644 --- a/test/faker/default/test_faker_unique_generator.rb +++ b/test/faker/default/test_faker_unique_generator.rb @@ -121,4 +121,43 @@ def stubbed_generator2.test assert_equal(2, generator2.test) end end + + def test_thread_safety + stubbed_generator = Object.new + def stubbed_generator.test + 1 + end + + generator = Faker::UniqueGenerator.new(stubbed_generator, 3) + + Thread.new do + assert_equal(1, generator.test) + + assert_raises Faker::UniqueGenerator::RetryLimitExceeded do + generator.test + end + end.join + + Thread.new do + assert_equal(1, generator.test) + end.join + + assert_equal(1, generator.test) + + assert_raises Faker::UniqueGenerator::RetryLimitExceeded do + generator.test + end + + Thread.new do + generator.clear + end.join + + assert_raises Faker::UniqueGenerator::RetryLimitExceeded do + generator.test + end + + generator.clear + + assert_equal(1, generator.test) + end end diff --git a/test/test_determinism.rb b/test/test_determinism.rb index d106f33473..120140dd46 100644 --- a/test/test_determinism.rb +++ b/test/test_determinism.rb @@ -22,6 +22,26 @@ def test_determinism end end + def test_thread_safety + expected_values = 2.times.map do |index| + Faker::Config.random = Random.new(index) + Faker::Number.digit + end + + threads = expected_values.each_with_index.map do |expected_value, index| + Thread.new do + 100_000.times.each do + Faker::Config.random = Random.new(index) + output = Faker::Number.digit + + assert_equal output, expected_value + end + end + end + + threads.each(&:join) + end + private def deterministic_random?(first, method_name)