├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bg.gemspec ├── bin ├── console └── setup ├── lib ├── bg.rb └── bg │ ├── asyncable.rb │ ├── deferrable.rb │ ├── deferred_method_call_job.rb │ └── version.rb └── test ├── backgroundable_object.rb ├── bg ├── asyncable_test.rb ├── deferrable_test.rb └── deferred_method_call_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | log/* 19 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.3 3 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop 4 | # to ignore them, so only the ones explicitly set in this file are enabled. 5 | DisabledByDefault: true 6 | Exclude: 7 | - '**/bin/*' 8 | - '**/db/**/*' 9 | - '**/templates/**/*' 10 | - '**/vendor/**/*' 11 | 12 | # Prefer &&/|| over and/or. 13 | Style/AndOr: 14 | Enabled: true 15 | 16 | # Do not use braces for hash literals when they are the last argument of a 17 | # method call. 18 | Style/BracesAroundHashParameters: 19 | Enabled: true 20 | 21 | # Align `when` with `case`. 22 | Style/CaseIndentation: 23 | Enabled: true 24 | 25 | # No extra empty lines. 26 | Style/EmptyLines: 27 | Enabled: true 28 | 29 | # In a regular class definition, no empty lines around the body. 30 | Style/EmptyLinesAroundClassBody: 31 | Enabled: true 32 | 33 | # In a regular module definition, no empty lines around the body. 34 | Style/EmptyLinesAroundModuleBody: 35 | Enabled: true 36 | 37 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. 38 | Style/HashSyntax: 39 | Enabled: true 40 | 41 | # Method definitions after `private` or `protected` isolated calls need one 42 | # extra level of indentation. 43 | Style/IndentationConsistency: 44 | Enabled: true 45 | EnforcedStyle: rails 46 | 47 | # Two spaces, no tabs (for indentation). 48 | Style/IndentationWidth: 49 | Enabled: true 50 | 51 | # Defining a method with parameters needs parentheses. 52 | Style/MethodDefParentheses: 53 | Enabled: true 54 | 55 | # Use `foo {}` not `foo{}`. 56 | Style/SpaceBeforeBlockBraces: 57 | Enabled: true 58 | 59 | # Use `foo { bar }` not `foo {bar}`. 60 | Style/SpaceInsideBlockBraces: 61 | Enabled: true 62 | 63 | # Use `{ a: 1 }` not `{a:1}`. 64 | Style/SpaceInsideHashLiteralBraces: 65 | Enabled: true 66 | 67 | # Check quotes usage according to lint rule below. 68 | Style/StringLiterals: 69 | Enabled: true 70 | EnforcedStyle: double_quotes 71 | 72 | # Detect hard tabs, no hard tabs. 73 | Style/Tab: 74 | Enabled: true 75 | 76 | # Blank lines should not have any spaces. 77 | Style/TrailingBlankLines: 78 | Enabled: true 79 | 80 | # No trailing whitespace. 81 | Style/TrailingWhitespace: 82 | Enabled: true 83 | 84 | # Use quotes for string literals when they are enough. 85 | Style/UnneededPercentQ: 86 | Enabled: true 87 | 88 | # Align `end` with the matching keyword or starting expression except for 89 | # assignments, where it should be aligned with the LHS. 90 | Lint/EndAlignment: 91 | Enabled: true 92 | AlignWith: variable 93 | 94 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 95 | Lint/RequireParentheses: 96 | Enabled: true 97 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | env: LC_ALL="en_US.UTF-8" LANG="en_US.UTF-8" 3 | install: bundle install --jobs=1 --retry=3 4 | rvm: 5 | - 2.3.1 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Declare your gem's dependencies in bg.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | # gem 'byebug', group: [:development, :test] 15 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Nathan Hopkins 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Lines of Code](http://img.shields.io/badge/lines_of_code-117-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/) 2 | [![Maintainability](https://api.codeclimate.com/v1/badges/1144d074fee9298347a7/maintainability)](https://codeclimate.com/github/hopsoft/bg/maintainability) 3 | [![Build Status](http://img.shields.io/travis/hopsoft/bg.svg?style=flat)](https://travis-ci.org/hopsoft/bg) 4 | [![Coverage Status](https://img.shields.io/coveralls/hopsoft/bg.svg?style=flat)](https://coveralls.io/r/hopsoft/bg?branch=master) 5 | [![Downloads](http://img.shields.io/gem/dt/bg.svg?style=flat)](http://rubygems.org/gems/bg) 6 | 7 | # Bg 8 | 9 | ## Non-blocking ActiveRecord method invocation 10 | 11 | This library allows you to invoke ActiveRecord instance methods in the background. 12 | 13 | * `Bg::Asyncable` uses concurrent-ruby to execute methods in a different thread 14 | * `Bg::Deferrable` uses ActiveJob to execute methods in a background process 15 | 16 | 17 | ## Quickstart 18 | 19 | ### Setup 20 | 21 | ```ruby 22 | class User < ApplicationRecord 23 | include Bg::Asyncable::Behavior # uses concurrent-ruby 24 | include Bg::Deferrable::Behavior # uses ActiveJob 25 | end 26 | ``` 27 | 28 | ### Usage 29 | 30 | ```ruby 31 | user = User.find(params[:id]) 32 | 33 | # blocking in-process 34 | user.do_hard_work 35 | 36 | # non-blocking in-process separate thread 37 | user.async.do_hard_work 38 | 39 | # non-blocking out-of-process background job 40 | user.defer.do_hard_work 41 | user.defer(queue: :low, wait: 5.minutes).do_hard_work 42 | ``` 43 | 44 | ## Deferrable 45 | 46 | `Bg::Deferrable` leverages [GlobalID::Identification](https://github.com/rails/globalid) to marshal ActiveRecord instances across process boundaries. 47 | This means that state is not shared between the main process & the process actually executing the method. 48 | 49 | * __Do not__ depend on lexically scoped bindings when invoking methods. 50 | * __Do not__ pass unmarshallable types as arguments. 51 | `Bg::Deferrable` will prepare arguments for enqueuing, but best practice is to follow 52 | Sidekiq's [simple parameters](https://github.com/mperham/sidekiq/wiki/Best-Practices#1-make-your-job-parameters-small-and-simple) rule. 53 | 54 | ### Examples 55 | 56 | #### Good 57 | 58 | ```ruby 59 | user = User.find(params[:id]) 60 | user.defer.do_hard_work 1, true, "foo" 61 | ``` 62 | 63 | #### Bad 64 | 65 | ```ruby 66 | user = User.find(params[:id]) 67 | # in memory changes will not be available in Bg::Deferrable invoked methods 68 | user.name = "new value" 69 | 70 | # args may not marshal properly 71 | user.defer.do_hard_work :foo, Time.now, instance_of_complex_type 72 | 73 | user.defer.do_hard_work do 74 | # blocks are not supported 75 | end 76 | ``` 77 | 78 | ## Asyncable 79 | 80 | `Bg::Asyncable` disallows invoking methods that take blocks as an argument. 81 | 82 | * __Important:__ It's your responsibility to protect shared data between threads 83 | 84 | ### Examples 85 | 86 | #### Good 87 | 88 | ```ruby 89 | user = User.find(params[:id]) 90 | user.name = "new value" 91 | user.async.do_hard_work 1, true, "foo" 92 | user.async.do_hard_work :foo, Time.now, instance_of_complex_type 93 | ``` 94 | 95 | #### Bad 96 | 97 | ```ruby 98 | user = User.find(params[:id]) 99 | 100 | user.async.do_hard_work do 101 | # blocks are not supported 102 | end 103 | ``` 104 | 105 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.test_files = FileList["tests/**/test_*.rb"] 6 | t.libs.push "test" 7 | t.pattern = "test/**/*_test.rb" 8 | t.warning = true 9 | t.verbose = true 10 | end 11 | 12 | task default: :test 13 | -------------------------------------------------------------------------------- /bg.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/bg/version" 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = "bg" 5 | gem.license = "MIT" 6 | gem.version = Bg::VERSION 7 | gem.authors = ["Nathan Hopkins"] 8 | gem.email = ["natehop@gmail.com"] 9 | gem.homepage = "https://github.com/hopsoft/bg" 10 | gem.summary = "" 11 | 12 | gem.files = Dir["lib/**/*.rb", "bin/*", "[A-Z]*"] 13 | gem.test_files = Dir["test/**/*.rb"] 14 | 15 | gem.add_dependency "activerecord", ">= 5.0" 16 | gem.add_dependency "activejob", ">= 5.0" 17 | gem.add_dependency "globalid", ">= 0.3" 18 | gem.add_dependency "concurrent-ruby", ">= 1.0" 19 | 20 | gem.add_development_dependency "rake" 21 | gem.add_development_dependency "purdytest" 22 | gem.add_development_dependency "coveralls" 23 | gem.add_development_dependency "pry" 24 | gem.add_development_dependency "pry-nav" 25 | gem.add_development_dependency "pry-stack_explorer" 26 | end 27 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "bg" 5 | 6 | require "pry" 7 | Pry.start 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/bg.rb: -------------------------------------------------------------------------------- 1 | require "bg/version" 2 | require "bg/asyncable" 3 | require "bg/deferrable" 4 | 5 | module Bg 6 | end 7 | -------------------------------------------------------------------------------- /lib/bg/asyncable.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | require "concurrent" 3 | 4 | module Bg 5 | class Asyncable 6 | class Wrapper 7 | include ::Concurrent::Async 8 | 9 | def initialize(object, wait: 0) 10 | # IMPORTANT: call super without any arguments 11 | # https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Async.html 12 | super() 13 | @object = object 14 | @wait = wait.to_f 15 | end 16 | 17 | def invoke_method(name, *args) 18 | sleep @wait if @wait > 0 19 | base = self.is_a?(::ActiveRecord::Base) ? self.class : ::ActiveRecord::Base 20 | base.connection_pool.with_connection do 21 | @object.send name, *args 22 | end 23 | end 24 | end 25 | 26 | module Behavior 27 | def async(wait: 0) 28 | ::Bg::Asyncable.new(self, wait: wait.to_f) 29 | end 30 | end 31 | 32 | def initialize(object, wait: 0) 33 | @object = object 34 | @wait = wait.to_f 35 | end 36 | 37 | def method_missing(name, *args) 38 | if @object.respond_to? name 39 | raise ::ArgumentError.new("blocks are not supported") if block_given? 40 | begin 41 | wrapped = ::Bg::Asyncable::Wrapper.new(@object, wait: @wait) 42 | wrapped.async.invoke_method name, *args 43 | rescue ::StandardError => e 44 | raise ::ArgumentError.new("Failed to execute method asynchronously! <#{@object.class.name}##{name}> #{e.message}") 45 | ensure 46 | return 47 | end 48 | end 49 | super 50 | end 51 | 52 | def respond_to?(name) 53 | return true if @object.respond_to? name 54 | super 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/bg/deferrable.rb: -------------------------------------------------------------------------------- 1 | require "globalid" 2 | require "bg/deferred_method_call_job" 3 | 4 | module Bg 5 | class Deferrable 6 | module Behavior 7 | # Enqueues the method call to be executed by a DeferredMethodCallJob background worker. 8 | def defer(queue: :default, wait: 0) 9 | ::Bg::Deferrable.new self, queue: queue, wait: wait 10 | end 11 | end 12 | 13 | def self.make_enqueable(value) 14 | case value 15 | when ::Hash then 16 | value.each.with_object({}) do |(key, val), memo| 17 | memo[key.to_s] = make_enqueable(val) 18 | end 19 | when ::Array then 20 | value.map { |val| make_enqueable val } 21 | when ::Symbol then 22 | value.to_s 23 | when ::Date, ::Time, ::DateTime then 24 | value.respond_to?(:iso8601) ? value.iso8601 : value.to_s 25 | else 26 | value 27 | end 28 | end 29 | 30 | def initialize(object, queue: :default, wait: 0) 31 | raise ::ArgumentError unless object.is_a?(::GlobalID::Identification) 32 | @object = object 33 | @queue = queue || :default 34 | @wait = wait.to_i 35 | end 36 | 37 | def method_missing(name, *args) 38 | if @object.respond_to? name 39 | raise ::ArgumentError.new("blocks are not supported") if block_given? 40 | begin 41 | queue_args = { queue: @queue } 42 | queue_args[:wait] = @wait if @wait > 0 43 | job = ::Bg::DeferredMethodCallJob.set(**queue_args).perform_later @object, name.to_s, *self.class.make_enqueable(args) 44 | rescue ::StandardError => e 45 | raise ::ArgumentError.new("Failed to background method call! <#{@object.class.name}##{name}> #{e.message}") 46 | ensure 47 | return job 48 | end 49 | end 50 | super 51 | end 52 | 53 | def respond_to?(name) 54 | return true if @object.respond_to? name 55 | super 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/bg/deferred_method_call_job.rb: -------------------------------------------------------------------------------- 1 | require "active_job" 2 | 3 | module Bg 4 | class DeferredMethodCallJob < ::ActiveJob::Base 5 | queue_as :default 6 | 7 | def perform(object, method, *args) 8 | object.send method, *args 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/bg/version.rb: -------------------------------------------------------------------------------- 1 | module Bg 2 | VERSION = "0.0.5" 3 | end 4 | -------------------------------------------------------------------------------- /test/backgroundable_object.rb: -------------------------------------------------------------------------------- 1 | require "globalid" 2 | 3 | class Bg::BackgroundableObject 4 | include ::GlobalID::Identification 5 | 6 | def self.find(id) 7 | new id 8 | end 9 | 10 | attr_accessor :id 11 | 12 | def initialize(id) 13 | @id = id 14 | end 15 | 16 | def update(attrs={}) 17 | true 18 | end 19 | 20 | def wait(seconds=0) 21 | sleep seconds.to_i 22 | true 23 | end 24 | 25 | def eigen 26 | class << self 27 | self 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/bg/asyncable_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | class Bg::AsyncableTest < ::ActiveSupport::TestCase 4 | 5 | test "slow io bound method invocations run in parallel" do 6 | obj = ::Bg::BackgroundableObject.new(:example) 7 | obj.eigen.send :include, ::Bg::Asyncable::Behavior 8 | start = Time.now 9 | 10.times { obj.async.wait 1 } 10 | assert (Time.now - start) <= 1.1 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /test/bg/deferrable_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | class Bg::DeferrableTest < ::ActiveSupport::TestCase 4 | 5 | setup do 6 | @deferrable = ::Bg::Deferrable.new(::Bg::BackgroundableObject.new(:example)) 7 | end 8 | 9 | test ".make_enqueable with Symbol" do 10 | value = ::Bg::Deferrable.make_enqueable(:foo) 11 | assert value == "foo" 12 | end 13 | 14 | test ".make_enqueable with Date" do 15 | date = ::Date.today 16 | value = ::Bg::Deferrable.make_enqueable(date) 17 | assert value == date.iso8601 18 | end 19 | 20 | test ".make_enqueable with Time" do 21 | time = ::Time.now 22 | value = ::Bg::Deferrable.make_enqueable(time) 23 | assert value == time.iso8601 24 | end 25 | 26 | test ".make_enqueable with DateTime" do 27 | date_time = ::DateTime.now 28 | value = ::Bg::Deferrable.make_enqueable(date_time) 29 | assert value == date_time.iso8601 30 | end 31 | 32 | test ".make_enqueable with Array" do 33 | date_time = ::DateTime.now 34 | list = [:foo, "bar", true, date_time] 35 | value = ::Bg::Deferrable.make_enqueable(list) 36 | assert value == ["foo", "bar", true, date_time.iso8601] 37 | end 38 | 39 | test ".make_enqueable with nested Array" do 40 | date_time = ::DateTime.now 41 | list = [:foo, "bar", true, date_time] 42 | list << list.dup 43 | value = ::Bg::Deferrable.make_enqueable(list) 44 | expected = ["foo", "bar", true, date_time.iso8601] 45 | expected << expected.dup 46 | assert value == expected 47 | end 48 | 49 | test ".make_enqueable with Hash" do 50 | date_time = ::DateTime.now 51 | hash = { a: :foo, b: "bar", c: true, d: date_time } 52 | value = ::Bg::Deferrable.make_enqueable(hash) 53 | assert value == { "a" => "foo", "b" => "bar", "c" => true, "d" => date_time.iso8601 } 54 | end 55 | 56 | test ".make_enqueable with nested Hash" do 57 | date_time = ::DateTime.now 58 | hash = { a: :foo, b: "bar", c: true, d: date_time } 59 | hash[:e] = hash.dup 60 | value = ::Bg::Deferrable.make_enqueable(hash) 61 | expected = { "a" => "foo", "b" => "bar", "c" => true, "d" => date_time.iso8601 } 62 | expected["e"] = expected.dup 63 | assert value == expected 64 | end 65 | 66 | test ".make_enqueable with complex Hash" do 67 | time = ::Time.now 68 | hash = { a: :foo, b: time, c: { a: :bar, b: time.dup }, d: [:baz, time.dup, {a: :wat, b: time.dup, c: [a: time.dup]}] } 69 | value = ::Bg::Deferrable.make_enqueable(hash) 70 | expected = { 71 | "a" => "foo", 72 | "b" => time.iso8601, 73 | "c" => { "a" => "bar", "b" => time.iso8601 }, 74 | "d" => [ "baz", time.iso8601, { "a" => "wat", "b" => time.iso8601, "c" => [{"a" => time.iso8601}] } ] 75 | } 76 | assert value == expected 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /test/bg/deferred_method_call_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | require "active_job/test_helper" 3 | 4 | class Bg::DeferredMethodCallJobTest < ::ActiveJob::TestCase 5 | 6 | test "enqueues with no args" do 7 | assert_enqueued_with job: ::Bg::DeferredMethodCallJob do 8 | obj = ::Bg::BackgroundableObject.new(:example) 9 | obj.eigen.send :include, ::Bg::Deferrable::Behavior 10 | obj.defer.update 11 | end 12 | end 13 | 14 | test "enqueues with simple args" do 15 | assert_enqueued_with job: ::Bg::DeferredMethodCallJob do 16 | obj = ::Bg::BackgroundableObject.new(:example) 17 | obj.eigen.send :include, ::Bg::Deferrable::Behavior 18 | obj.defer.update foo: true, bar: "baz" 19 | end 20 | end 21 | 22 | test "enqueues with globalid args" do 23 | assert_enqueued_with job: ::Bg::DeferredMethodCallJob do 24 | parent = ::Bg::BackgroundableObject.new(:parent) 25 | parent.eigen.send :include, ::Bg::Deferrable::Behavior 26 | parent.defer.update child: ::Bg::BackgroundableObject.new(:child) 27 | end 28 | end 29 | 30 | test "enqueues with complex args" do 31 | assert_enqueued_with job: ::Bg::DeferredMethodCallJob do 32 | parent = ::Bg::BackgroundableObject.new(:parent) 33 | parent.eigen.send :include, ::Bg::Deferrable::Behavior 34 | parent.defer.update children: [::Bg::BackgroundableObject.new(:child1), ::Bg::BackgroundableObject.new(:child2)], 35 | foo: { bar: [:baz, Date.new, Time.new, DateTime.new] } 36 | end 37 | end 38 | 39 | test "#perform_now properly invokes the method" do 40 | obj = ::Bg::BackgroundableObject.new(:example) 41 | assert ::Bg::DeferredMethodCallJob.perform_now(obj, :update) 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "coveralls" 2 | Coveralls.wear! 3 | require_relative "../lib/bg" 4 | require_relative "backgroundable_object" 5 | require "minitest/autorun" 6 | require "purdytest" 7 | require "pry" 8 | require "pry-nav" 9 | require "pry-stack_explorer" 10 | 11 | ::ActiveSupport::TestCase.test_order = :random 12 | ::GlobalID.app = "test" 13 | ::ActiveRecord::Base = Class.new do 14 | def connection_pool 15 | Class.new do 16 | def with_connection 17 | yield 18 | end 19 | end 20 | end 21 | end 22 | --------------------------------------------------------------------------------