aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml5
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock44
-rw-r--r--Rakefile4
-rw-r--r--actioncable/CHANGELOG.md13
-rw-r--r--actioncable/README.md2
-rw-r--r--actioncable/lib/action_cable/channel/base.rb38
-rw-r--r--actioncable/lib/action_cable/channel/streams.rb2
-rw-r--r--actioncable/lib/action_cable/connection.rb2
-rw-r--r--actioncable/lib/action_cable/connection/base.rb2
-rw-r--r--actioncable/lib/action_cable/connection/faye_client_socket.rb48
-rw-r--r--actioncable/lib/action_cable/connection/faye_event_loop.rb44
-rw-r--r--actioncable/lib/action_cable/connection/stream.rb57
-rw-r--r--actioncable/lib/action_cable/connection/stream_event_loop.rb46
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb6
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb4
-rw-r--r--actioncable/lib/action_cable/server/base.rb10
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb18
-rw-r--r--actioncable/lib/action_cable/server/worker.rb2
-rw-r--r--actioncable/test/channel/base_test.rb15
-rw-r--r--actioncable/test/channel/periodic_timers_test.rb1
-rw-r--r--actioncable/test/channel/rejection_test.rb2
-rw-r--r--actioncable/test/channel/stream_test.rb31
-rw-r--r--actioncable/test/client_test.rb139
-rw-r--r--actioncable/test/connection/client_socket_test.rb4
-rw-r--r--actioncable/test/connection/stream_test.rb2
-rw-r--r--actioncable/test/server/base_test.rb33
-rw-r--r--actioncable/test/stubs/test_server.rb12
-rw-r--r--actioncable/test/subscription_adapter/common.rb4
-rw-r--r--actioncable/test/subscription_adapter/evented_redis_test.rb8
-rw-r--r--actioncable/test/test_helper.rb48
-rw-r--r--actionpack/CHANGELOG.md7
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb2
-rw-r--r--actionpack/lib/action_controller/test_case.rb8
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb7
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb2
-rw-r--r--actionpack/test/controller/test_case_test.rb5
-rw-r--r--actionpack/test/dispatch/routing_test.rb15
-rw-r--r--actionpack/test/journey/router_test.rb2
-rw-r--r--actionview/CHANGELOG.md18
-rw-r--r--actionview/lib/action_view/digestor.rb6
-rw-r--r--actionview/lib/action_view/railtie.rb2
-rw-r--r--actionview/lib/action_view/template.rb8
-rw-r--r--actionview/test/fixtures/test/render_file_inspect_local_assigns.erb1
-rw-r--r--actionview/test/fixtures/test/render_file_unicode_local.erb1
-rw-r--r--actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb1
-rw-r--r--actionview/test/template/compiled_templates_test.rb19
-rw-r--r--activemodel/lib/active_model/errors.rb2
-rw-r--r--activemodel/lib/active_model/type/float.rb9
-rw-r--r--activemodel/test/cases/errors_test.rb10
-rw-r--r--activerecord/CHANGELOG.md4
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb14
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb7
-rw-r--r--activerecord/lib/active_record/integration.rb23
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb1
-rw-r--r--activerecord/lib/active_record/migration.rb1
-rw-r--r--activerecord/lib/active_record/query_cache.rb10
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb8
-rw-r--r--activerecord/lib/active_record/type.rb4
-rw-r--r--activerecord/lib/active_record/type/helpers.rb5
-rw-r--r--activerecord/lib/active_record/type/internal/abstract_json.rb4
-rw-r--r--activerecord/lib/active_record/type/serialized.rb4
-rw-r--r--activerecord/lib/active_record/type/value.rb5
-rw-r--r--activerecord/test/cases/adapters/postgresql/transaction_test.rb21
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb34
-rw-r--r--activerecord/test/cases/integration_test.rb6
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb14
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb4
-rw-r--r--activerecord/test/cases/test_case.rb7
-rw-r--r--activerecord/test/cases/type/date_time_test.rb2
-rw-r--r--activerecord/test/models/bulb.rb6
-rw-r--r--activesupport/lib/active_support/callbacks.rb336
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb6
-rw-r--r--activesupport/lib/active_support/deprecation/instance_delegator.rb13
-rw-r--r--activesupport/lib/active_support/execution_wrapper.rb33
-rw-r--r--activesupport/lib/active_support/lazy_load_hooks.rb4
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb30
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb4
-rw-r--r--activesupport/test/callbacks_test.rb73
-rw-r--r--activesupport/test/executor_test.rb55
-rw-r--r--guides/source/2_2_release_notes.md1
-rw-r--r--guides/source/5_0_release_notes.md2
-rw-r--r--guides/source/action_cable_overview.md2
-rw-r--r--guides/source/active_record_querying.md1
-rw-r--r--guides/source/active_support_instrumentation.md13
-rw-r--r--guides/source/configuring.md2
-rw-r--r--guides/source/testing.md4
-rw-r--r--railties/CHANGELOG.md4
-rw-r--r--railties/lib/rails/commands/server/server_command.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/test.tt4
-rw-r--r--railties/test/generators/plugin_test_runner_test.rb6
99 files changed, 998 insertions, 594 deletions
diff --git a/.travis.yml b/.travis.yml
index f01b58ecb3..585791f757 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -35,10 +35,8 @@ env:
matrix:
- "GEM=railties"
- "GEM=ap"
- - "GEM=ac"
- - "GEM=ac FAYE=1"
+ - "GEM=ac TESTOPTS=-vs26"
- "GEM=ac:integration"
- - "GEM=ac:integration FAYE=1"
- "GEM=am,amo,as,av,aj"
- "GEM=as PRESERVE_TIMEZONES=1"
- "GEM=ar:mysql2"
@@ -69,7 +67,6 @@ matrix:
- rvm: ruby-head
- rvm: jruby-9.0.5.0
- env: "GEM=ac:integration"
- - env: "GEM=ac:integration FAYE=1"
fast_finish: true
notifications:
diff --git a/Gemfile b/Gemfile
index a3cbb69c74..e4d625d47c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -44,7 +44,7 @@ end
# Active Support.
gem "dalli", ">= 2.2.1"
-gem "listen", "~> 3.0.5", require: false
+gem "listen", ">= 3.0.5", "< 3.2", require: false
# Active Job.
group :job do
@@ -72,10 +72,7 @@ group :cable do
gem "hiredis", require: false
gem "redis", require: false
- gem "faye-websocket", require: false
-
- # Lock to 1.1.1 until the fix for https://github.com/faye/faye/issues/394 is released
- gem "faye", "1.1.1", require: false
+ gem "websocket-client-simple", require: false
gem "blade", require: false, platforms: [:ruby]
gem "blade-sauce_labs_plugin", require: false, platforms: [:ruby]
diff --git a/Gemfile.lock b/Gemfile.lock
index 68c75997b1..14635a8cb2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,10 +1,10 @@
GIT
remote: https://github.com/QueueClassic/queue_classic.git
- revision: 1ef197b9db8149a895e59077badcb5b94d4c8b44
+ revision: 51d56ca6fa2fdf1eeffdffd702ae1cc0940b5156
branch: master
specs:
queue_classic (3.2.0.RC1)
- pg (>= 0.17, < 0.19)
+ pg (>= 0.17, < 0.20)
GIT
remote: https://github.com/collectiveidea/delayed_job.git
@@ -31,7 +31,7 @@ GIT
GIT
remote: https://github.com/resque/resque.git
- revision: a3a66389618b830de0e6acf862b0dc9fde05cf49
+ revision: 20d885065ac19e7f7d7a982f4ed1296083db0300
specs:
resque (1.27.0)
mono_logger (~> 1.0)
@@ -112,8 +112,9 @@ GEM
addressable (2.4.0)
amq-protocol (2.0.1)
arel (7.1.2)
- backburner (1.3.0)
+ backburner (1.3.1)
beaneater (~> 1.0)
+ concurrent-ruby (~> 1.0.1)
dante (> 0.1.5)
bcrypt (3.1.11)
bcrypt (3.1.11-x64-mingw32)
@@ -141,7 +142,7 @@ GEM
builder (3.2.2)
bunny (2.2.2)
amq-protocol (>= 2.0.1)
- byebug (9.0.5)
+ byebug (9.0.6)
childprocess (0.5.9)
ffi (~> 1.0, >= 1.0.11)
coffee-rails (4.2.1)
@@ -170,13 +171,14 @@ GEM
em-socksify (0.3.1)
eventmachine (>= 1.0.0.beta.4)
erubis (2.7.0)
+ event_emitter (0.2.5)
eventmachine (1.2.0.1)
eventmachine (1.2.0.1-x64-mingw32)
eventmachine (1.2.0.1-x86-mingw32)
execjs (2.7.0)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
- faye (1.1.1)
+ faye (1.2.2)
cookiejar (>= 0.3.0)
em-http-request (>= 0.3.0)
eventmachine (>= 0.12.0)
@@ -203,9 +205,10 @@ GEM
kindlerb (0.1.1)
mustache
nokogiri
- listen (3.0.8)
+ listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
+ ruby_dep (~> 1.2)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.4)
@@ -236,9 +239,9 @@ GEM
nokogiri (1.6.8-x86-mingw32)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
- pg (0.18.4)
- pg (0.18.4-x64-mingw32)
- pg (0.18.4-x86-mingw32)
+ pg (0.19.0)
+ pg (0.19.0-x64-mingw32)
+ pg (0.19.0-x86-mingw32)
pkg-config (1.1.7)
psych (2.1.1)
puma (3.6.0)
@@ -262,7 +265,7 @@ GEM
nokogiri (~> 1.6.0)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- rake (11.2.2)
+ rake (11.3.0)
rb-fsevent (0.9.7)
rdoc (4.2.2)
json (~> 1.4)
@@ -275,6 +278,7 @@ GEM
redis (~> 3.3)
resque (~> 1.26)
rufus-scheduler (~> 3.2)
+ ruby_dep (1.4.0)
rubyzip (1.2.0)
rufus-scheduler (3.2.2)
sass-rails (5.0.6)
@@ -290,10 +294,10 @@ GEM
childprocess (~> 0.5)
rubyzip (~> 1.0)
websocket (~> 1.0)
- sequel (4.38.0)
+ sequel (4.39.0)
serverengine (1.5.11)
sigdump (~> 0.2.2)
- sidekiq (4.2.0)
+ sidekiq (4.2.2)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (~> 1.5)
@@ -318,7 +322,7 @@ GEM
sqlite3 (1.3.11)
sqlite3 (1.3.11-x64-mingw32)
sqlite3 (1.3.11-x86-mingw32)
- stackprof (0.2.9)
+ stackprof (0.2.10)
sucker_punch (2.0.2)
concurrent-ruby (~> 1.0.0)
thin (1.7.0)
@@ -334,7 +338,7 @@ GEM
turbolinks-source (5.0.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- tzinfo-data (1.2016.6)
+ tzinfo-data (1.2016.7)
tzinfo (>= 1.0.0)
uglifier (3.0.2)
execjs (>= 0.3.0, < 3)
@@ -346,6 +350,9 @@ GEM
nokogiri
wdm (0.1.1)
websocket (1.2.3)
+ websocket-client-simple (0.3.0)
+ event_emitter
+ websocket
websocket-driver (0.6.4)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
@@ -370,12 +377,10 @@ DEPENDENCIES
delayed_job!
delayed_job_active_record!
em-hiredis
- faye (= 1.1.1)
- faye-websocket
hiredis
jquery-rails
kindlerb (= 0.1.1)
- listen (~> 3.0.5)
+ listen (>= 3.0.5, < 3.2)
minitest (< 5.3.4)
mocha (~> 0.14)
mysql2 (>= 0.4.4)
@@ -409,6 +414,7 @@ DEPENDENCIES
uglifier (>= 1.3.0)
w3c_validators
wdm (>= 0.1.0)
+ websocket-client-simple
BUNDLED WITH
- 1.13.1
+ 1.13.2
diff --git a/Rakefile b/Rakefile
index 3bc4cd19fb..202eb5e6fc 100644
--- a/Rakefile
+++ b/Rakefile
@@ -23,9 +23,6 @@ task default: %w(test test:isolated)
FRAMEWORKS.each do |project|
system(%(cd #{project} && #{$0} #{task_name} --trace)) || errors << project
end
- if task_name =~ /test/
- system(%(cd actioncable && env FAYE=1 #{$0} #{task_name} --trace)) || errors << "actioncable-faye"
- end
fail("Errors in #{errors.join(', ')}") unless errors.empty?
end
end
@@ -36,7 +33,6 @@ task :smoke do
system %(cd #{project} && #{$0} test:isolated --trace)
end
system %(cd activerecord && #{$0} sqlite3:isolated_test --trace)
- system %(cd actioncable && env FAYE=1 #{$0} test:isolated --trace)
end
desc "Install gems for all projects."
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index dec6f7c027..137c88d91b 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,3 +1,16 @@
+* Prevent race where the client could receive and act upon a
+ subscription confirmation before the channel's `subscribed` method
+ completed.
+
+ Fixes #25381.
+
+ *Vladimir Dementyev*
+
+* Buffer writes to websocket connections, to avoid blocking threads
+ that could be doing more useful things.
+
+ *Matthew Draper*, *Tinco Andringa*
+
* Protect against concurrent writes to a websocket connection from
multiple threads; the underlying OS write is not always threadsafe.
diff --git a/actioncable/README.md b/actioncable/README.md
index 28e2602cbf..a0b7412dd4 100644
--- a/actioncable/README.md
+++ b/actioncable/README.md
@@ -167,7 +167,7 @@ App.cable.subscriptions.create "AppearanceChannel",
buttonSelector = "[data-behavior~=appear_away]"
install: ->
- $(document).on "page:change.appearance", =>
+ $(document).on "turbolinks:load.appearance", =>
@appear()
$(document).on "click.appearance", buttonSelector, =>
diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb
index 2e589a2cfa..a866044f95 100644
--- a/actioncable/lib/action_cable/channel/base.rb
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -144,13 +144,14 @@ module ActionCable
# When a channel is streaming via pubsub, we want to delay the confirmation
# transmission until pubsub subscription is confirmed.
- @defer_subscription_confirmation = false
+ #
+ # The counter starts at 1 because it's awaiting a call to #subscribe_to_channel
+ @defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1)
@reject_subscription = nil
@subscription_confirmation_sent = nil
delegate_connection_identifiers
- subscribe_to_channel
end
# Extract the action name from the passed data and process it via the channel. The process will ensure
@@ -169,6 +170,17 @@ module ActionCable
end
end
+ # This method is called after subscription has been added to the connection
+ # and confirms or rejects the subscription.
+ def subscribe_to_channel
+ run_callbacks :subscribe do
+ subscribed
+ end
+
+ reject_subscription if subscription_rejected?
+ ensure_confirmation_sent
+ end
+
# Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
# This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
def unsubscribe_from_channel # :nodoc:
@@ -201,12 +213,18 @@ module ActionCable
end
end
+ def ensure_confirmation_sent
+ return if subscription_rejected?
+ @defer_subscription_confirmation_counter.decrement
+ transmit_subscription_confirmation unless defer_subscription_confirmation?
+ end
+
def defer_subscription_confirmation!
- @defer_subscription_confirmation = true
+ @defer_subscription_confirmation_counter.increment
end
def defer_subscription_confirmation?
- @defer_subscription_confirmation
+ @defer_subscription_confirmation_counter.value > 0
end
def subscription_confirmation_sent?
@@ -230,18 +248,6 @@ module ActionCable
end
end
- def subscribe_to_channel
- run_callbacks :subscribe do
- subscribed
- end
-
- if subscription_rejected?
- reject_subscription
- else
- transmit_subscription_confirmation unless defer_subscription_confirmation?
- end
- end
-
def extract_action(data)
(data["action"].presence || :receive).to_sym
end
diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb
index 13deb62662..dbba333353 100644
--- a/actioncable/lib/action_cable/channel/streams.rb
+++ b/actioncable/lib/action_cable/channel/streams.rb
@@ -84,7 +84,7 @@ module ActionCable
connection.server.event_loop.post do
pubsub.subscribe(broadcasting, handler, lambda do
- transmit_subscription_confirmation
+ ensure_confirmation_sent
logger.info "#{self.class.name} is streaming from #{broadcasting}"
end)
end
diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb
index 5f813cf8e0..902efb07e2 100644
--- a/actioncable/lib/action_cable/connection.rb
+++ b/actioncable/lib/action_cable/connection.rb
@@ -8,8 +8,6 @@ module ActionCable
autoload :ClientSocket
autoload :Identification
autoload :InternalChannel
- autoload :FayeClientSocket
- autoload :FayeEventLoop
autoload :MessageBuffer
autoload :Stream
autoload :StreamEventLoop
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index 4c7fcc1434..06f4f5edd3 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -57,7 +57,7 @@ module ActionCable
@worker_pool = server.worker_pool
@logger = new_tagged_logger
- @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class)
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
diff --git a/actioncable/lib/action_cable/connection/faye_client_socket.rb b/actioncable/lib/action_cable/connection/faye_client_socket.rb
deleted file mode 100644
index 06e92c5d52..0000000000
--- a/actioncable/lib/action_cable/connection/faye_client_socket.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require "faye/websocket"
-
-module ActionCable
- module Connection
- class FayeClientSocket
- def initialize(env, event_target, stream_event_loop, protocols)
- @env = env
- @event_target = event_target
- @protocols = protocols
-
- @faye = nil
- end
-
- def alive?
- @faye && @faye.ready_state == Faye::WebSocket::API::OPEN
- end
-
- def transmit(data)
- connect
- @faye.send data
- end
-
- def close
- @faye && @faye.close
- end
-
- def protocol
- @faye && @faye.protocol
- end
-
- def rack_response
- connect
- @faye.rack_response
- end
-
- private
- def connect
- return if @faye
- @faye = Faye::WebSocket.new(@env, @protocols)
-
- @faye.on(:open) { |event| @event_target.on_open }
- @faye.on(:message) { |event| @event_target.on_message(event.data) }
- @faye.on(:close) { |event| @event_target.on_close(event.reason, event.code) }
- @faye.on(:error) { |event| @event_target.on_error(event.message) }
- end
- end
- end
-end
diff --git a/actioncable/lib/action_cable/connection/faye_event_loop.rb b/actioncable/lib/action_cable/connection/faye_event_loop.rb
deleted file mode 100644
index cfbe26ee6a..0000000000
--- a/actioncable/lib/action_cable/connection/faye_event_loop.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-require "thread"
-
-require "eventmachine"
-EventMachine.epoll if EventMachine.epoll?
-EventMachine.kqueue if EventMachine.kqueue?
-
-module ActionCable
- module Connection
- class FayeEventLoop
- @@mutex = Mutex.new
-
- def timer(interval, &block)
- ensure_reactor_running
- EMTimer.new(::EM::PeriodicTimer.new(interval, &block))
- end
-
- def post(task = nil, &block)
- task ||= block
-
- ensure_reactor_running
- ::EM.next_tick(&task)
- end
-
- private
- def ensure_reactor_running
- return if EventMachine.reactor_running?
- @@mutex.synchronize do
- Thread.new { EventMachine.run } unless EventMachine.reactor_running?
- Thread.pass until EventMachine.reactor_running?
- end
- end
-
- class EMTimer
- def initialize(inner)
- @inner = inner
- end
-
- def shutdown
- @inner.cancel
- end
- end
- end
- end
-end
diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb
index 5a2aace0ba..d66e1b4e41 100644
--- a/actioncable/lib/action_cable/connection/stream.rb
+++ b/actioncable/lib/action_cable/connection/stream.rb
@@ -14,6 +14,9 @@ module ActionCable
@rack_hijack_io = nil
@write_lock = Mutex.new
+
+ @write_head = nil
+ @write_buffer = Queue.new
end
def each(&callback)
@@ -30,14 +33,62 @@ module ActionCable
end
def write(data)
- @write_lock.synchronize do
- return @rack_hijack_io.write(data) if @rack_hijack_io
- return @stream_send.call(data) if @stream_send
+ if @stream_send
+ return @stream_send.call(data)
end
+
+ if @write_lock.try_lock
+ begin
+ if @write_head.nil? && @write_buffer.empty?
+ written = @rack_hijack_io.write_nonblock(data, exception: false)
+
+ case written
+ when :wait_writable
+ # proceed below
+ when data.bytesize
+ return data.bytesize
+ else
+ @write_head = data.byteslice(written, data.bytesize)
+ @event_loop.writes_pending @rack_hijack_io
+
+ return data.bytesize
+ end
+ end
+ ensure
+ @write_lock.unlock
+ end
+ end
+
+ @write_buffer << data
+ @event_loop.writes_pending @rack_hijack_io
+
+ data.bytesize
rescue EOFError, Errno::ECONNRESET
@socket_object.client_gone
end
+ def flush_write_buffer
+ @write_lock.synchronize do
+ loop do
+ if @write_head.nil?
+ return true if @write_buffer.empty?
+ @write_head = @write_buffer.pop
+ end
+
+ written = @rack_hijack_io.write_nonblock(@write_head, exception: false)
+ case written
+ when :wait_writable
+ return false
+ when @write_head.bytesize
+ @write_head = nil
+ else
+ @write_head = @write_head.byteslice(written, @write_head.bytesize)
+ return false
+ end
+ end
+ end
+ end
+
def receive(data)
@socket_object.parse(data)
end
diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb
index 106b948c45..eec24638b6 100644
--- a/actioncable/lib/action_cable/connection/stream_event_loop.rb
+++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb
@@ -5,7 +5,7 @@ module ActionCable
module Connection
class StreamEventLoop
def initialize
- @nio = @thread = nil
+ @nio = @executor = @thread = nil
@map = {}
@stopping = false
@todo = Queue.new
@@ -20,13 +20,14 @@ module ActionCable
def post(task = nil, &block)
task ||= block
- Concurrent.global_io_executor << task
+ spawn
+ @executor << task
end
def attach(io, stream)
@todo << lambda do
- @map[io] = stream
- @nio.register(io, :r)
+ @map[io] = @nio.register(io, :r)
+ @map[io].value = stream
end
wakeup
end
@@ -39,6 +40,15 @@ module ActionCable
wakeup
end
+ def writes_pending(io)
+ @todo << lambda do
+ if monitor = @map[io]
+ monitor.interests = :rw
+ end
+ end
+ wakeup
+ end
+
def stop
@stopping = true
wakeup if @nio
@@ -52,6 +62,13 @@ module ActionCable
return if @thread && @thread.status
@nio ||= NIO::Selector.new
+
+ @executor ||= Concurrent::ThreadPoolExecutor.new(
+ min_threads: 1,
+ max_threads: 10,
+ max_queue: 0,
+ )
+
@thread = Thread.new { run }
return true
@@ -77,12 +94,25 @@ module ActionCable
monitors.each do |monitor|
io = monitor.io
- stream = @map[io]
+ stream = monitor.value
begin
- stream.receive io.read_nonblock(4096)
- rescue IO::WaitReadable
- next
+ if monitor.writable?
+ if stream.flush_write_buffer
+ monitor.interests = :r
+ end
+ next unless monitor.readable?
+ end
+
+ incoming = io.read_nonblock(4096, exception: false)
+ case incoming
+ when :wait_readable
+ next
+ when nil
+ stream.close
+ else
+ stream.receive incoming
+ end
rescue
# We expect one of EOFError or Errno::ECONNRESET in
# normal operation (when the client goes away). But if
diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb
index 9060183249..00511aead5 100644
--- a/actioncable/lib/action_cable/connection/subscriptions.rb
+++ b/actioncable/lib/action_cable/connection/subscriptions.rb
@@ -26,10 +26,14 @@ module ActionCable
id_key = data["identifier"]
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
+ return if subscriptions.key?(id_key)
+
subscription_klass = id_options[:channel].safe_constantize
if subscription_klass && ActionCable::Channel::Base >= subscription_klass
- subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options)
+ subscription = subscription_klass.new(connection, id_key, id_options)
+ subscriptions[id_key] = subscription
+ subscription.subscribe_to_channel
else
logger.error "Subscription class not found: #{id_options[:channel].inspect}"
end
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 52d8daad4b..382141b89f 100644
--- a/actioncable/lib/action_cable/connection/web_socket.rb
+++ b/actioncable/lib/action_cable/connection/web_socket.rb
@@ -4,8 +4,8 @@ module ActionCable
module Connection
# Wrap the real socket to minimize the externally-presented API
class WebSocket
- def initialize(env, event_target, event_loop, client_socket_class, protocols: ActionCable::INTERNAL[:protocols])
- @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop, protocols) : nil
+ def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols])
+ @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, event_loop, protocols) : nil
end
def possible?
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
index dd059a553b..419eccd73c 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -37,9 +37,13 @@ module ActionCable
connections.each(&:close)
@mutex.synchronize do
- worker_pool.halt if @worker_pool
-
+ # Shutdown the worker pool
+ @worker_pool.halt if @worker_pool
@worker_pool = nil
+
+ # Shutdown the pub/sub adapter
+ @pubsub.shutdown if @pubsub
+ @pubsub = nil
end
end
@@ -49,7 +53,7 @@ module ActionCable
end
def event_loop
- @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
+ @event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new }
end
# The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
index 7153593d4c..dc146f07b0 100644
--- a/actioncable/lib/action_cable/server/configuration.rb
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -4,7 +4,7 @@ module ActionCable
# in a Rails config initializer.
class Configuration
attr_accessor :logger, :log_tags
- attr_accessor :use_faye, :connection_class, :worker_pool_size
+ attr_accessor :connection_class, :worker_pool_size
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
attr_accessor :cable, :url, :mount_path
@@ -35,22 +35,6 @@ module ActionCable
adapter = "PostgreSQL" if adapter == "Postgresql"
"ActionCable::SubscriptionAdapter::#{adapter}".constantize
end
-
- def event_loop_class
- if use_faye
- ActionCable::Connection::FayeEventLoop
- else
- ActionCable::Connection::StreamEventLoop
- end
- end
-
- def client_socket_class
- if use_faye
- ActionCable::Connection::FayeClientSocket
- else
- ActionCable::Connection::ClientSocket
- end
- end
end
end
end
diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb
index 7460472551..43639c27af 100644
--- a/actioncable/lib/action_cable/server/worker.rb
+++ b/actioncable/lib/action_cable/server/worker.rb
@@ -25,7 +25,7 @@ module ActionCable
# Stop processing work: any work that has not already started
# running will be discarded from the queue
def halt
- @executor.kill
+ @executor.shutdown
end
def stopping?
diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb
index 2bb3214f74..9a3a3581e6 100644
--- a/actioncable/test/channel/base_test.rb
+++ b/actioncable/test/channel/base_test.rb
@@ -77,11 +77,13 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
@channel = ChatChannel.new @connection, "{id: 1}", id: 1
end
- test "should subscribe to a channel on initialize" do
+ test "should subscribe to a channel" do
+ @channel.subscribe_to_channel
assert_equal 1, @channel.room.id
end
test "on subscribe callbacks" do
+ @channel.subscribe_to_channel
assert @channel.subscribed
end
@@ -90,6 +92,8 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
end
test "unsubscribing from a channel" do
+ @channel.subscribe_to_channel
+
assert @channel.room
assert @channel.subscribed?
@@ -150,8 +154,13 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
assert_equal expected, @connection.last_transmission
end
- test "subscription confirmation" do
+ test "do not send subscription confirmation on initialize" do
+ assert_nil @connection.last_transmission
+ end
+
+ test "subscription confirmation on subscribe_to_channel" do
expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" }
+ @channel.subscribe_to_channel
assert_equal expected, @connection.last_transmission
end
@@ -208,6 +217,8 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
test "notification for transmit_subscription_confirmation" do
begin
+ @channel.subscribe_to_channel
+
events = []
ActiveSupport::Notifications.subscribe "transmit_subscription_confirmation.action_cable" do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb
index 529abb9535..2ee711fd29 100644
--- a/actioncable/test/channel/periodic_timers_test.rb
+++ b/actioncable/test/channel/periodic_timers_test.rb
@@ -62,6 +62,7 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase
@connection.server.event_loop.expects(:timer).times(3).returns(stub(shutdown: nil))
channel = ChatChannel.new @connection, "{id: 1}", id: 1
+ channel.subscribe_to_channel
channel.unsubscribe_from_channel
assert_equal [], channel.send(:active_periodic_timers)
end
diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb
index faf35ad048..99c4a7603a 100644
--- a/actioncable/test/channel/rejection_test.rb
+++ b/actioncable/test/channel/rejection_test.rb
@@ -20,6 +20,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase
test "subscription rejection" do
@connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) }
@channel = SecretChannel.new @connection, "{id: 1}", id: 1
+ @channel.subscribe_to_channel
expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" }
assert_equal expected, @connection.last_transmission
@@ -28,6 +29,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase
test "does not execute action if subscription is rejected" do
@connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) }
@channel = SecretChannel.new @connection, "{id: 1}", id: 1
+ @channel.subscribe_to_channel
expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" }
assert_equal expected, @connection.last_transmission
diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb
index da26f81a5c..31dcde2e29 100644
--- a/actioncable/test/channel/stream_test.rb
+++ b/actioncable/test/channel/stream_test.rb
@@ -53,6 +53,7 @@ module ActionCable::StreamTests
connection = TestConnection.new
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
channel = ChatChannel.new connection, "{id: 1}", id: 1
+ channel.subscribe_to_channel
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
channel.unsubscribe_from_channel
@@ -64,6 +65,7 @@ module ActionCable::StreamTests
connection = TestConnection.new
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("channel", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
channel = SymbolChannel.new connection, ""
+ channel.subscribe_to_channel
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
channel.unsubscribe_from_channel
@@ -76,6 +78,7 @@ module ActionCable::StreamTests
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:stream_tests:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
channel = ChatChannel.new connection, ""
+ channel.subscribe_to_channel
channel.stream_for Room.new(1)
end
end
@@ -84,7 +87,9 @@ module ActionCable::StreamTests
run_in_eventmachine do
connection = TestConnection.new
- ChatChannel.new connection, "{id: 1}", id: 1
+ channel = ChatChannel.new connection, "{id: 1}", id: 1
+ channel.subscribe_to_channel
+
assert_nil connection.last_transmission
wait_for_async
@@ -114,7 +119,7 @@ module ActionCable::StreamTests
end
end
- require "action_cable/subscription_adapter/inline"
+ require "action_cable/subscription_adapter/async"
class UserCallbackChannel < ActionCable::Channel::Base
def subscribed
@@ -124,9 +129,16 @@ module ActionCable::StreamTests
end
end
- class StreamEncodingTest < ActionCable::TestCase
+ class MultiChatChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from "main_room"
+ stream_from "test_all_rooms"
+ end
+ end
+
+ class StreamFromTest < ActionCable::TestCase
setup do
- @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Inline)
+ @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Async)
@server.config.allowed_request_origins = %w( http://rubyonrails.com )
end
@@ -153,6 +165,17 @@ module ActionCable::StreamTests
end
end
+ test "subscription confirmation should only be sent out once with muptiple stream_from" do
+ run_in_eventmachine do
+ connection = open_connection
+ expected = { "identifier" => { "channel" => MultiChatChannel.name }.to_json, "type" => "confirm_subscription" }
+ connection.websocket.expects(:transmit).with(expected.to_json)
+ receive(connection, command: "subscribe", channel: MultiChatChannel.name, identifiers: {})
+
+ wait_for_async
+ end
+ end
+
private
def subscribe_to(connection, identifiers:)
receive connection, command: "subscribe", identifiers: identifiers
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
index 2e3821828f..f6d4ab3202 100644
--- a/actioncable/test/client_test.rb
+++ b/actioncable/test/client_test.rb
@@ -1,13 +1,38 @@
require "test_helper"
require "concurrent"
-require "faye/websocket"
+require "websocket-client-simple"
require "json"
require "active_support/hash_with_indifferent_access"
+####
+# 😷 Warning suppression 😷
+WebSocket::Frame::Handler::Handler03.prepend Module.new {
+ def initialize(*)
+ @application_data_buffer = nil
+ super
+ end
+}
+
+WebSocket::Frame::Data.prepend Module.new {
+ def initialize(*)
+ @masking_key = nil
+ super
+ end
+}
+
+WebSocket::Client::Simple::Client.prepend Module.new {
+ def initialize(*)
+ @socket = nil
+ super
+ end
+}
+#
+####
+
class ClientTest < ActionCable::TestCase
- WAIT_WHEN_EXPECTING_EVENT = 8
+ WAIT_WHEN_EXPECTING_EVENT = 2
WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5
class EchoChannel < ActionCable::Channel::Base
@@ -39,20 +64,9 @@ class ClientTest < ActionCable::TestCase
server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
server.config.cable = ActiveSupport::HashWithIndifferentAccess.new(adapter: "async")
- server.config.use_faye = ENV["FAYE"].present?
# and now the "real" setup for our test:
server.config.disable_request_forgery_protection = true
-
- Thread.new { EventMachine.run } unless EventMachine.reactor_running?
- Thread.pass until EventMachine.reactor_running?
-
- # faye-websocket is warning-rich
- @previous_verbose, $VERBOSE = $VERBOSE, nil
- end
-
- def teardown
- $VERBOSE = @previous_verbose
end
def with_puma_server(rack_app = ActionCable.server, port = 3099)
@@ -73,44 +87,49 @@ class ClientTest < ActionCable::TestCase
attr_reader :pings
def initialize(port)
- @ws = Faye::WebSocket::Client.new("ws://127.0.0.1:#{port}/")
- @messages = Queue.new
- @closed = Concurrent::Event.new
- @has_messages = Concurrent::Semaphore.new(0)
- @pings = 0
-
- open = Concurrent::Event.new
- error = nil
-
- @ws.on(:error) do |event|
- if open.set?
- @messages << RuntimeError.new(event.message)
- else
- error = event.message
- open.set
+ messages = @messages = Queue.new
+ closed = @closed = Concurrent::Event.new
+ has_messages = @has_messages = Concurrent::Semaphore.new(0)
+ pings = @pings = Concurrent::AtomicFixnum.new(0)
+
+ open = Concurrent::Promise.new
+
+ @ws = WebSocket::Client::Simple.connect("ws://127.0.0.1:#{port}/") do |ws|
+ ws.on(:error) do |event|
+ event = RuntimeError.new(event.message) unless event.is_a?(Exception)
+
+ if open.pending?
+ open.fail(event)
+ else
+ messages << event
+ has_messages.release
+ end
end
- end
- @ws.on(:open) do |event|
- open.set
- end
+ ws.on(:open) do |event|
+ open.set(true)
+ end
- @ws.on(:message) do |event|
- message = JSON.parse(event.data)
- if message["type"] == "ping"
- @pings += 1
- else
- @messages << message
- @has_messages.release
+ ws.on(:message) do |event|
+ if event.type == :close
+ closed.set
+ else
+ message = JSON.parse(event.data)
+ if message["type"] == "ping"
+ pings.increment
+ else
+ messages << message
+ has_messages.release
+ end
+ end
end
- end
- @ws.on(:close) do |event|
- @closed.set
+ ws.on(:close) do |event|
+ closed.set
+ end
end
- open.wait(WAIT_WHEN_EXPECTING_EVENT)
- raise error if error
+ open.wait!(WAIT_WHEN_EXPECTING_EVENT)
end
def read_message
@@ -161,13 +180,17 @@ class ClientTest < ActionCable::TestCase
end
end
- def faye_client(port)
+ def websocket_client(port)
SyncClient.new(port)
end
+ def concurrently(enum)
+ enum.map { |*x| Concurrent::Future.execute { yield(*x) } }.map(&:value!)
+ end
+
def test_single_client
with_puma_server do |port|
- c = faye_client(port)
+ c = websocket_client(port)
assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message)
@@ -179,12 +202,12 @@ class ClientTest < ActionCable::TestCase
def test_interacting_clients
with_puma_server do |port|
- clients = 10.times.map { faye_client(port) }
+ clients = concurrently(10.times) { websocket_client(port) }
barrier_1 = Concurrent::CyclicBarrier.new(clients.size)
barrier_2 = Concurrent::CyclicBarrier.new(clients.size)
- clients.map { |c| Concurrent::Future.execute {
+ concurrently(clients) do |c|
assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
assert_equal({ "identifier"=>'{"channel":"ClientTest::EchoChannel"}', "type"=>"confirm_subscription" }, c.read_message)
@@ -194,38 +217,38 @@ class ClientTest < ActionCable::TestCase
c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "bulk", message: "hello")
barrier_2.wait WAIT_WHEN_EXPECTING_EVENT
assert_equal clients.size, c.read_messages(clients.size).size
- } }.each(&:wait!)
+ end
- clients.map { |c| Concurrent::Future.execute { c.close } }.each(&:wait!)
+ concurrently(clients, &:close)
end
end
def test_many_clients
with_puma_server do |port|
- clients = 100.times.map { faye_client(port) }
+ clients = concurrently(100.times) { websocket_client(port) }
- clients.map { |c| Concurrent::Future.execute {
+ concurrently(clients) do |c|
assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
assert_equal({ "identifier"=>'{"channel":"ClientTest::EchoChannel"}', "type"=>"confirm_subscription" }, c.read_message)
c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
assert_equal({ "identifier"=>'{"channel":"ClientTest::EchoChannel"}', "message"=>{ "dong"=>"hello" } }, c.read_message)
- } }.each(&:wait!)
+ end
- clients.map { |c| Concurrent::Future.execute { c.close } }.each(&:wait!)
+ concurrently(clients, &:close)
end
end
def test_disappearing_client
with_puma_server do |port|
- c = faye_client(port)
+ c = websocket_client(port)
assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message)
c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "delay", message: "hello")
c.close # disappear before write
- c = faye_client(port)
+ c = websocket_client(port)
assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message)
@@ -240,7 +263,7 @@ class ClientTest < ActionCable::TestCase
app = ActionCable.server
identifier = JSON.generate(channel: "ClientTest::EchoChannel")
- c = faye_client(port)
+ c = websocket_client(port)
assert_equal({ "type" => "welcome" }, c.read_message)
c.send_message command: "subscribe", identifier: identifier
assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message)
@@ -261,7 +284,7 @@ class ClientTest < ActionCable::TestCase
def test_server_restart
with_puma_server do |port|
- c = faye_client(port)
+ c = websocket_client(port)
assert_equal({ "type" => "welcome" }, c.read_message)
c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message)
diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb
index 5043a63370..dff7fefbfb 100644
--- a/actioncable/test/connection/client_socket_test.rb
+++ b/actioncable/test/connection/client_socket_test.rb
@@ -33,8 +33,6 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
end
test "delegate socket errors to on_error handler" do
- skip if ENV["FAYE"].present?
-
run_in_eventmachine do
connection = open_connection
@@ -49,8 +47,6 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
end
test "closes hijacked i/o socket at shutdown" do
- skip if ENV["FAYE"].present?
-
run_in_eventmachine do
connection = open_connection
diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb
index 4128b32f15..36e1d3c095 100644
--- a/actioncable/test/connection/stream_test.rb
+++ b/actioncable/test/connection/stream_test.rb
@@ -34,8 +34,6 @@ class ActionCable::Connection::StreamTest < ActionCable::TestCase
[ EOFError, Errno::ECONNRESET ].each do |closed_exception|
test "closes socket on #{closed_exception}" do
- skip if ENV["FAYE"].present?
-
run_in_eventmachine do
connection = open_connection
diff --git a/actioncable/test/server/base_test.rb b/actioncable/test/server/base_test.rb
new file mode 100644
index 0000000000..f0a51c5a7d
--- /dev/null
+++ b/actioncable/test/server/base_test.rb
@@ -0,0 +1,33 @@
+require "test_helper"
+require "stubs/test_server"
+require "active_support/core_ext/hash/indifferent_access"
+
+class BaseTest < ActiveSupport::TestCase
+ def setup
+ @server = ActionCable::Server::Base.new
+ @server.config.cable = { adapter: "async" }.with_indifferent_access
+ end
+
+ class FakeConnection
+ def close
+ end
+ end
+
+ test "#restart closes all open connections" do
+ conn = FakeConnection.new
+ @server.add_connection(conn)
+
+ conn.expects(:close)
+ @server.restart
+ end
+
+ test "#restart shuts down worker pool" do
+ @server.worker_pool.expects(:halt)
+ @server.restart
+ end
+
+ test "#restart shuts down pub/sub adapter" do
+ @server.pubsub.expects(:shutdown)
+ @server.restart
+ end
+end
diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb
index b64ff33789..5bf2a151dc 100644
--- a/actioncable/test/stubs/test_server.rb
+++ b/actioncable/test/stubs/test_server.rb
@@ -10,12 +10,6 @@ class TestServer
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
@config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter)
- @config.use_faye = ENV["FAYE"].present?
- @config.client_socket_class = if @config.use_faye
- ActionCable::Connection::FayeClientSocket
- else
- ActionCable::Connection::ClientSocket
- end
@mutex = Monitor.new
end
@@ -25,10 +19,8 @@ class TestServer
end
def event_loop
- @event_loop ||= if @config.use_faye
- ActionCable::Connection::FayeEventLoop.new
- else
- ActionCable::Connection::StreamEventLoop.new
+ @event_loop ||= ActionCable::Connection::StreamEventLoop.new.tap do |loop|
+ loop.instance_variable_set(:@executor, Concurrent.global_io_executor)
end
end
diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb
index 1538157995..3aa88c2caa 100644
--- a/actioncable/test/subscription_adapter/common.rb
+++ b/actioncable/test/subscription_adapter/common.rb
@@ -11,7 +11,7 @@ module CommonSubscriptionAdapterTest
def setup
server = ActionCable::Server::Base.new
server.config.cable = cable_config.with_indifferent_access
- server.config.use_faye = ENV["FAYE"].present?
+ server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
adapter_klass = server.config.pubsub_adapter
@@ -20,7 +20,7 @@ module CommonSubscriptionAdapterTest
end
def teardown
- [@rx_adapter, @tx_adapter].uniq.each(&:shutdown)
+ [@rx_adapter, @tx_adapter].uniq.compact.each(&:shutdown)
end
def subscribe_as_queue(channel, adapter = @rx_adapter)
diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb
index d6ca0e77cb..f316bc46ef 100644
--- a/actioncable/test/subscription_adapter/evented_redis_test.rb
+++ b/actioncable/test/subscription_adapter/evented_redis_test.rb
@@ -12,6 +12,14 @@ class EventedRedisAdapterTest < ActionCable::TestCase
end
def teardown
+ super
+
+ # Ensure EM is shut down before we re-enable warnings
+ EventMachine.reactor_thread.tap do |thread|
+ EventMachine.stop
+ thread.join
+ end
+
$VERBOSE = @previous_verbose
end
diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb
index 39855ea252..a47032753b 100644
--- a/actioncable/test/test_helper.rb
+++ b/actioncable/test/test_helper.rb
@@ -13,41 +13,7 @@ end
# Require all the stubs and models
Dir[File.dirname(__FILE__) + "/stubs/*.rb"].each { |file| require file }
-if ENV["FAYE"].present?
- require "faye/websocket"
- class << Faye::WebSocket
- remove_method :ensure_reactor_running
-
- # We don't want Faye to start the EM reactor in tests because it makes testing much harder.
- # We want to be able to start and stop EM loop in tests to make things simpler.
- def ensure_reactor_running
- # no-op
- end
- end
-end
-
-module EventMachineConcurrencyHelpers
- def wait_for_async
- EM.run_deferred_callbacks
- end
-
- def run_in_eventmachine
- failure = nil
- EM.run do
- begin
- yield
- rescue => ex
- failure = ex
- ensure
- wait_for_async
- EM.stop if EM.reactor_running?
- end
- end
- raise failure if failure
- end
-end
-
-module ConcurrentRubyConcurrencyHelpers
+class ActionCable::TestCase < ActiveSupport::TestCase
def wait_for_async
wait_for_executor Concurrent.global_io_executor
end
@@ -56,18 +22,14 @@ module ConcurrentRubyConcurrencyHelpers
yield
wait_for_async
end
-end
-
-class ActionCable::TestCase < ActiveSupport::TestCase
- if ENV["FAYE"].present?
- include EventMachineConcurrencyHelpers
- else
- include ConcurrentRubyConcurrencyHelpers
- end
def wait_for_executor(executor)
+ # do not wait forever, wait 2s
+ timeout = 2
until executor.completed_task_count == executor.scheduled_task_count
sleep 0.1
+ timeout -= 0.1
+ raise "Executor could not complete all tasks in 2 seconds" unless timeout > 0
end
end
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index e7b8e1b628..5ffbc2ea5d 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,10 @@
+* Show an "unmatched constraints" error when params fail to match constraints
+ on a matched route, rather than a "missing keys" error.
+
+ Fixes #26470.
+
+ *Chris Carter*
+
* Fix adding implicitly rendered template digests to ETags.
Fixes a case when modifying an implicitly rendered template for a
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 15377ddcb9..f8a037189c 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -71,8 +71,6 @@ module ActionController
# format.csv { render csv: @csvable, filename: @csvable.name }
# end
# end
- # To use renderers and their mime types in more concise ways, see
- # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt>
def self.add(key, &block)
define_method(_render_with_renderer_method_name(key), &block)
RENDERERS << key.to_sym
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 09f2a79d85..f4ec13c831 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -498,10 +498,6 @@ module ActionController
parameters ||= {}
- if format
- parameters[:format] = format
- end
-
@html_document = nil
cookies.update(@request.cookies)
@@ -521,6 +517,10 @@ module ActionController
format ||= as
end
+ if format
+ parameters[:format] = format
+ end
+
parameters = parameters.symbolize_keys
generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s))
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index a289c34e8b..dc8b24b089 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -44,8 +44,12 @@ module ActionDispatch
return [route.format(parameterized_parts), params]
end
+ unmatched_keys = (missing_keys || []) & constraints.keys
+ missing_keys = (missing_keys || []) - unmatched_keys
+
message = "No route matches #{Hash[constraints.sort_by { |k,v| k.to_s }].inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
+ message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
+ message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
raise ActionController::UrlGenerationError, message
end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index bd4c781267..1925ffd9dd 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -2,8 +2,9 @@ require "securerandom"
require "active_support/core_ext/string/access"
module ActionDispatch
- # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
- # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header.
+ # Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible
+ # through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends
+ # the same id to the client via the X-Request-Id header.
#
# The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
@@ -12,7 +13,7 @@ module ActionDispatch
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
# from multiple pieces of the stack.
class RequestId
- X_REQUEST_ID = "X-Request-Id".freeze # :nodoc:
+ X_REQUEST_ID = "X-Request-Id".freeze #:nodoc:
def initialize(app)
@app = app
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 5abf59402d..a1bc357c8b 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -210,7 +210,7 @@ module ActionDispatch
}
constraints = Hash[@route.requirements.merge(params).sort_by { |k,v| k.to_s }]
message = "No route matches #{constraints.inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}"
+ message << ", missing required keys: #{missing_keys.sort.inspect}"
raise ActionController::UrlGenerationError, message
end
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index 738d8bab6d..d929885aea 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -646,6 +646,11 @@ XML
assert_equal 2, @request.request_parameters[:num_value]
end
+ def test_using_as_json_sets_format_json
+ post :render_body, params: { bool_value: true, str_value: "string", num_value: 2 }, as: :json
+ assert_equal "json", @request.format
+ end
+
def test_mutating_content_type_headers_for_plain_text_files_sets_the_header
@request.headers["Content-Type"] = "text/plain"
post :render_body, params: { name: "foo.txt" }
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 6ba52e37b6..9b6e060955 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -4684,22 +4684,25 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
include Routes.url_helpers
- test "url helpers raise a helpful error message when generation fails" do
+ test "url helpers raise a 'missing keys' error for a nil param with optimized helpers" do
url, missing = { action: "show", controller: "products", id: nil }, [:id]
- message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}"
+ message = "No route matches #{url.inspect}, missing required keys: #{missing.inspect}"
- # Optimized url helper
error = assert_raises(ActionController::UrlGenerationError) { product_path(nil) }
assert_equal message, error.message
+ end
+
+ test "url helpers raise a 'constraint failure' error for a nil param with non-optimized helpers" do
+ url, missing = { action: "show", controller: "products", id: nil }, [:id]
+ message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}"
- # Non-optimized url helper
error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil) }
assert_equal message, error.message
end
- test "url helpers raise message with mixed parameters when generation fails " do
+ test "url helpers raise message with mixed parameters when generation fails" do
url, missing = { action: "show", controller: "products", id: nil, "id"=>"url-tested" }, [:id]
- message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}"
+ message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}"
# Optimized url helper
error = assert_raises(ActionController::UrlGenerationError) { product_path(nil, "id"=>"url-tested") }
diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb
index 2b99637f56..7b5916eb72 100644
--- a/actionpack/test/journey/router_test.rb
+++ b/actionpack/test/journey/router_test.rb
@@ -297,7 +297,7 @@ module ActionDispatch
}
request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters)
- message = "No route matches #{Hash[request_parameters.sort_by { |k,v|k.to_s }].inspect} missing required keys: #{[missing_key.to_sym].inspect}"
+ message = "No route matches #{Hash[request_parameters.sort_by { |k,v|k.to_s }].inspect}, missing required keys: #{[missing_key.to_sym].inspect}"
error = assert_raises(ActionController::UrlGenerationError) do
@formatter.generate(
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 8bd4e1e56c..e93745c3bf 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,21 @@
+* Render now accepts any keys for locals, including reserved words
+
+ Only locals with valid variable names get set directly. Others
+ will still be available in local_assigns.
+
+ Example of render with reserved words:
+
+ ```erb
+ <%= render "example", class: "text-center", message: "Hello world!" %>
+
+ <!-- _example.html.erb: -->
+ <%= tag.div class: local_assigns[:class] do %>
+ <p><%= message %></p>
+ <% end %>
+ ```
+
+ *Peter Schilling*, *Matthew Draper*
+
* Show cache hits and misses when rendering partials.
Partials using the `cache` helper will show whether a render hit or missed
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 2d6ad8f6d9..0658d8601d 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -6,6 +6,12 @@ module ActionView
class Digestor
@@digest_mutex = Mutex.new
+ module PerExecutionDigestCacheExpiry
+ def self.before(target)
+ ActionView::LookupContext::DetailsKey.clear
+ end
+ end
+
class << self
# Supported options:
#
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index dfb99f4ea9..42795ca2c7 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -40,7 +40,7 @@ module ActionView
initializer "action_view.per_request_digest_cache" do |app|
ActiveSupport.on_load(:action_view) do
if app.config.consider_all_requests_local
- app.executor.to_run { ActionView::LookupContext::DetailsKey.clear }
+ app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry
end
end
end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 513935cef0..c01dd1c028 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -1,5 +1,6 @@
require "active_support/core_ext/object/try"
require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/delegation"
require "thread"
module ActionView
@@ -324,8 +325,13 @@ module ActionView
end
def locals_code #:nodoc:
+ # Only locals with valid variable names get set directly. Others will
+ # still be available in local_assigns.
+ locals = @locals.to_set - Module::DELEGATION_RESERVED_METHOD_NAMES
+ locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
+
# Double assign to suppress the dreaded 'assigned but unused variable' warning
- @locals.each_with_object("") { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
+ locals.each_with_object("") { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
end
def method_name #:nodoc:
diff --git a/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb b/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb
new file mode 100644
index 0000000000..aea5c351c5
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb
@@ -0,0 +1 @@
+<%= local_assigns.inspect.html_safe %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/render_file_unicode_local.erb b/actionview/test/fixtures/test/render_file_unicode_local.erb
new file mode 100644
index 0000000000..cbfd040a76
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_unicode_local.erb
@@ -0,0 +1 @@
+<%= 🎃 %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb b/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb
new file mode 100644
index 0000000000..7e3fe6c6d9
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb
@@ -0,0 +1 @@
+The class is <%= local_assigns[:class] %> \ No newline at end of file
diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
index 7e3e5883b4..3ecac46d34 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -9,6 +9,25 @@ class CompiledTemplatesTest < ActiveSupport::TestCase
assert_equal "This is nil: \n", render(template: "test/nil_return")
end
+ def test_template_with_ruby_keyword_locals
+ assert_equal "The class is foo",
+ render(file: "test/render_file_with_ruby_keyword_locals", locals: { class: "foo" })
+ end
+
+ def test_template_with_invalid_identifier_locals
+ locals = {
+ foo: "bar",
+ Foo: "bar",
+ "d-a-s-h-e-s": "",
+ "white space": "",
+ }
+ assert_equal locals.inspect, render(file: "test/render_file_inspect_local_assigns", locals: locals)
+ end
+
+ def test_template_with_unicode_identifier
+ assert_equal "🎂", render(file: "test/render_file_unicode_local", locals: { 🎃: "🎂" })
+ end
+
def test_template_gets_recompiled_when_using_different_keys_in_local_assigns
assert_equal "one", render(file: "test/render_file_with_locals_and_default")
assert_equal "two", render(file: "test/render_file_with_locals_and_default", locals: { secret: "two" })
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 191039f598..72746e194e 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -280,7 +280,7 @@ module ActiveModel
messages[attribute] = array.map { |message| full_message(attribute, message) }
end
else
- messages.dup
+ without_default_proc(messages)
end
end
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
index 94bb7e700c..4d0d2771a0 100644
--- a/activemodel/lib/active_model/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -7,6 +7,15 @@ module ActiveModel
:float
end
+ def type_cast_for_schema(value)
+ return "::Float::NAN" if value.try(:nan?)
+ case value
+ when ::Float::INFINITY then "::Float::INFINITY"
+ when -::Float::INFINITY then "-::Float::INFINITY"
+ else super
+ end
+ end
+
alias serialize cast
private
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 3288b5543c..95ca1f3969 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -250,6 +250,16 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal({ name: ["cannot be blank"] }, person.errors.to_hash)
end
+ test "to_hash returns a hash without default proc" do
+ person = Person.new
+ assert_nil person.errors.to_hash.default_proc
+ end
+
+ test "as_json returns a hash without default proc" do
+ person = Person.new
+ assert_nil person.errors.as_json.default_proc
+ end
+
test "full_messages creates a list of error messages with the attribute name included" do
person = Person.new
person.errors.add(:name, "cannot be blank")
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 4d0c1a4178..879c4a87cf 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Made ActiveRecord consistently use `ActiveRecord::Type` (not `ActiveModel::Type`)
+
+ *Iain Beeston*
+
* Serialize JSON attribute value `nil` as SQL `NULL`, not JSON `null`
*Trung Duc Tran*
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index e5b3af8252..278c95e27b 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -309,15 +309,12 @@ module ActiveRecord
def replace_on_target(record, index, skip_callbacks)
callback(:before_add, record) unless skip_callbacks
- was_loaded = loaded?
yield(record) if block_given?
- unless !was_loaded && loaded?
- if index
- @target[index] = record
- else
- @target << record
- end
+ if index
+ @target[index] = record
+ else
+ append_record(record)
end
callback(:after_add, record) unless skip_callbacks
@@ -514,6 +511,10 @@ module ActiveRecord
load_target.select { |r| ids.include?(r.id.to_s) }
end
end
+
+ def append_record(record)
+ @target << record unless @target.include?(record)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index d258eac0ed..1f264d325a 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -203,6 +203,10 @@ module ActiveRecord
def invertible_for?(record)
false
end
+
+ def append_record(record)
+ @target << record
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 10498f4322..05f0e974b6 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -19,7 +19,7 @@ module ActiveRecord
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
- return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
+ return false if ActiveRecord::Type::Boolean::FALSE_VALUES.include?(value)
!value.blank?
end
elsif value.respond_to?(:zero?)
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index c70178cd2c..945192fe04 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -26,7 +26,7 @@ module ActiveRecord
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
- # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump`
+ # * +class_name_or_coder+ - Optional, a coder object, which responds to +.load+ and +.dump+
# or a class name that the object type should be equal to.
#
# ==== Example
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 6ca53c72ce..2f8a89e88e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -65,7 +65,7 @@ module ActiveRecord
if @query_cache_enabled && !locked?(arel)
arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
- cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) }
+ cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) }
else
super
end
@@ -73,11 +73,17 @@ module ActiveRecord
private
- def cache_sql(sql, binds)
+ def cache_sql(sql, name, binds)
result =
if @query_cache[sql].key?(binds)
- ActiveSupport::Notifications.instrument("sql.active_record",
- sql: sql, binds: binds, name: "CACHE", connection_id: object_id)
+ ActiveSupport::Notifications.instrument(
+ "sql.active_record",
+ sql: sql,
+ binds: binds,
+ name: name,
+ connection_id: object_id,
+ cached: true,
+ )
@query_cache[sql][binds]
else
@query_cache[sql][binds] = yield
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 706b57842f..abd8cfc8f2 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -18,10 +18,13 @@ module ActiveRecord
#
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
- IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
+ IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN)
EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i
def ignore_payload?(payload)
- payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
+ payload[:exception] ||
+ payload[:cached] ||
+ IGNORED_PAYLOADS.include?(payload[:name]) ||
+ payload[:sql] !~ EXPLAINED_SQLS
end
ActiveSupport::Notifications.subscribe("sql.active_record", new)
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index e4c7a55541..3c54c6048d 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -53,18 +53,21 @@ module ActiveRecord
#
# Person.find(5).cache_key(:updated_at, :last_reviewed_at)
def cache_key(*timestamp_names)
- case
- when new_record?
+ if new_record?
"#{model_name.cache_key}/new"
- when timestamp_names.any?
- timestamp = max_updated_column_timestamp(timestamp_names)
- timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{model_name.cache_key}/#{id}-#{timestamp}"
- when timestamp = max_updated_column_timestamp
- timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{model_name.cache_key}/#{id}-#{timestamp}"
else
- "#{model_name.cache_key}/#{id}"
+ timestamp = if timestamp_names.any?
+ max_updated_column_timestamp(timestamp_names)
+ else
+ max_updated_column_timestamp
+ end
+
+ if timestamp
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
+ else
+ "#{model_name.cache_key}/#{id}"
+ end
end
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index f31931316c..b27e84a5be 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -35,6 +35,7 @@ module ActiveRecord
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
+ name = "CACHE #{name}" if payload[:cached]
sql = payload[:sql]
binds = nil
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 05568039d8..627e93b5b6 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,4 +1,5 @@
require "set"
+require "zlib"
require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/regexp"
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 387dd8e9bd..c45c8c1697 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -34,16 +34,14 @@ module ActiveRecord
def self.complete(enabled)
ActiveRecord::Base.connection.clear_query_cache
ActiveRecord::Base.connection.disable_query_cache! unless enabled
+
+ unless ActiveRecord::Base.connected? && ActiveRecord::Base.connection.transaction_open?
+ ActiveRecord::Base.clear_active_connections!
+ end
end
def self.install_executor_hooks(executor = ActiveSupport::Executor)
executor.register_hook(self)
-
- executor.to_complete do
- unless ActiveRecord::Base.connected? && ActiveRecord::Base.connection.transaction_open?
- ActiveRecord::Base.clear_active_connections!
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index a887be8a20..e4676f79a5 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -112,10 +112,6 @@ module ActiveRecord
# ...
# end
def calculate(operation, column_name)
- if column_name.is_a?(Symbol) && attribute_alias?(column_name)
- column_name = attribute_alias(column_name)
- end
-
if has_include?(column_name)
relation = construct_relation_for_association_calculations
relation = relation.distinct if operation.to_s.downcase == "count"
@@ -215,8 +211,8 @@ module ActiveRecord
def aggregate_column(column_name)
return column_name if Arel::Expressions === column_name
- if @klass.column_names.include?(column_name.to_s)
- Arel::Attribute.new(@klass.unscoped.table, column_name)
+ if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s)
+ @klass.arel_attribute(column_name)
else
Arel.sql(column_name == :all ? "*" : column_name.to_s)
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index 0b48d2186a..84373dddf2 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,4 +1,6 @@
require "active_model/type"
+require "active_record/type/helpers"
+require "active_record/type/value"
require "active_record/type/internal/abstract_json"
require "active_record/type/internal/timezone"
@@ -48,7 +50,6 @@ module ActiveRecord
end
end
- Helpers = ActiveModel::Type::Helpers
BigInteger = ActiveModel::Type::BigInteger
Binary = ActiveModel::Type::Binary
Boolean = ActiveModel::Type::Boolean
@@ -59,7 +60,6 @@ module ActiveRecord
String = ActiveModel::Type::String
Text = ActiveModel::Type::Text
UnsignedInteger = ActiveModel::Type::UnsignedInteger
- Value = ActiveModel::Type::Value
register(:big_integer, Type::BigInteger, override: false)
register(:binary, Type::Binary, override: false)
diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb
new file mode 100644
index 0000000000..a32ccd4bc3
--- /dev/null
+++ b/activerecord/lib/active_record/type/helpers.rb
@@ -0,0 +1,5 @@
+module ActiveRecord
+ module Type
+ Helpers = ActiveModel::Type::Helpers
+ end
+end
diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb
index e19c5a14da..67028546e4 100644
--- a/activerecord/lib/active_record/type/internal/abstract_json.rb
+++ b/activerecord/lib/active_record/type/internal/abstract_json.rb
@@ -1,8 +1,8 @@
module ActiveRecord
module Type
module Internal # :nodoc:
- class AbstractJson < ActiveModel::Type::Value # :nodoc:
- include ActiveModel::Type::Helpers::Mutable
+ class AbstractJson < Type::Value # :nodoc:
+ include Type::Helpers::Mutable
def type
:json
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index ac9134bfcb..ca12c83b1a 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module Type
- class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
- include ActiveModel::Type::Helpers::Mutable
+ class Serialized < DelegateClass(Type::Value) # :nodoc:
+ include Type::Helpers::Mutable
attr_reader :subtype, :coder
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
new file mode 100644
index 0000000000..89ef29106b
--- /dev/null
+++ b/activerecord/lib/active_record/type/value.rb
@@ -0,0 +1,5 @@
+module ActiveRecord
+ module Type
+ class Value < ActiveModel::Type::Value; end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
index d992e22305..00119f13bb 100644
--- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require "support/connection_helper"
+require "concurrent/atomic/cyclic_barrier"
module ActiveRecord
class PostgresqlTransactionTest < ActiveRecord::PostgreSQLTestCase
@@ -61,26 +62,28 @@ module ActiveRecord
test "raises Deadlocked when a deadlock is encountered" do
with_warning_suppression do
assert_raises(ActiveRecord::Deadlocked) do
+ barrier = Concurrent::CyclicBarrier.new(2)
+
s1 = Sample.create value: 1
s2 = Sample.create value: 2
thread = Thread.new do
Sample.transaction do
s1.lock!
- sleep 1
+ barrier.wait
s2.update_attributes value: 1
end
end
- sleep 0.5
-
- Sample.transaction do
- s2.lock!
- sleep 1
- s1.update_attributes value: 2
+ begin
+ Sample.transaction do
+ s2.lock!
+ barrier.wait
+ s1.update_attributes value: 2
+ end
+ ensure
+ thread.join
end
-
- thread.join
end
end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index fed59c2ab3..0ce67f971b 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -2461,9 +2461,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
test "double insertion of new object to association when same association used in the after create callback of a new object" do
- car = Car.create!
- car.bulbs << TrickyBulb.new
- assert_equal 1, car.bulbs.size
+ reset_callbacks(:save, Bulb) do
+ Bulb.after_save { |record| record.car.bulbs.to_a }
+ car = Car.create!
+ car.bulbs << Bulb.new
+ assert_equal 1, car.bulbs.size
+ end
end
def test_association_force_reload_with_only_true_is_deprecated
@@ -2510,9 +2513,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_no_queries { car.bulb_ids }
end
+ def test_loading_association_in_validate_callback_doesnt_affect_persistence
+ reset_callbacks(:validation, Bulb) do
+ Bulb.after_validation { |m| m.car.bulbs.load }
+
+ car = Car.create!(name: "Car")
+ bulb = car.bulbs.create!
+
+ assert_equal [bulb], car.bulbs
+ end
+ end
+
private
def force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.load_target
end
+
+ def reset_callbacks(kind, klass)
+ old_callbacks = {}
+ old_callbacks[klass] = klass.send("_#{kind}_callbacks").dup
+ klass.subclasses.each do |subclass|
+ old_callbacks[subclass] = subclass.send("_#{kind}_callbacks").dup
+ end
+ yield
+ ensure
+ klass.send("_#{kind}_callbacks=", old_callbacks[klass])
+ klass.subclasses.each do |subclass|
+ subclass.send("_#{kind}_callbacks=", old_callbacks[subclass])
+ end
+ end
end
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index 766917b196..00457965d7 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -172,4 +172,10 @@ class IntegrationTest < ActiveRecord::TestCase
owner = owners(:blackbeard)
assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at)
end
+
+ def test_cache_key_when_named_timestamp_is_nil
+ owner = owners(:blackbeard)
+ owner.happy_at = nil
+ assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at)
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 57b1bc889a..8b604ba930 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -423,6 +423,13 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase
t.datetime :datetime_with_default, default: "2014-06-05 07:17:04"
t.time :time_with_default, default: "07:17:04"
end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ @connection.create_table :infinity_defaults, force: true do |t|
+ t.float :float_with_inf_default, default: Float::INFINITY
+ t.float :float_with_nan_default, default: Float::NAN
+ end
+ end
end
teardown do
@@ -438,4 +445,11 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase
assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output
assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output
end
+
+ def test_schema_dump_with_float_column_infinity_default
+ skip unless current_adapter?(:PostgreSQLAdapter)
+ output = dump_table_schema('infinity_defaults')
+ assert_match %r{t\.float\s+"float_with_inf_default",\s+default: ::Float::INFINITY}, output
+ assert_match %r{t\.float\s+"float_with_nan_default",\s+default: ::Float::NAN}, output
+ end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index bebd856faf..8e9514de7c 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -313,8 +313,8 @@ class SerializedAttributeTest < ActiveRecord::TestCase
return if value.nil?
value.gsub(" encoded", "")
end
- type = Class.new(ActiveModel::Type::Value) do
- include ActiveModel::Type::Helpers::Mutable
+ type = Class.new(ActiveRecord::Type::Value) do
+ include ActiveRecord::Type::Helpers::Mutable
def serialize(value)
return if value.nil?
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 60ac3e08a1..8eddc5a9ed 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -125,12 +125,9 @@ module ActiveRecord
end
def call(name, start, finish, message_id, values)
- sql = values[:sql]
-
- # FIXME: this seems bad. we should probably have a better way to indicate
- # the query was cached
- return if "CACHE" == values[:name]
+ return if values[:cached]
+ sql = values[:sql]
self.class.log_all << sql
self.class.log << sql unless ignore.match?(sql)
end
diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb
index bc4900e1c2..6848619ece 100644
--- a/activerecord/test/cases/type/date_time_test.rb
+++ b/activerecord/test/cases/type/date_time_test.rb
@@ -3,7 +3,7 @@ require "models/task"
module ActiveRecord
module Type
- class IntegerTest < ActiveRecord::TestCase
+ class DateTimeTest < ActiveRecord::TestCase
def test_datetime_seconds_precision_applied_to_timestamp
skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported?
p = Task.create!(starting: ::Time.now)
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index 3196207ac9..113d21cb84 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -50,9 +50,3 @@ class FailedBulb < Bulb
throw(:abort)
end
end
-
-class TrickyBulb < Bulb
- after_create do |record|
- record.car.bulbs.to_a
- end
-end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index 6d39be1c1f..890b1cd73b 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -63,6 +63,8 @@ module ActiveSupport
included do
extend ActiveSupport::DescendantsTracker
+ class_attribute :__callbacks, instance_writer: false
+ self.__callbacks ||= {}
end
CALLBACK_FILTER_TYPES = [:before, :after, :around]
@@ -86,21 +88,57 @@ module ActiveSupport
# run_callbacks :save do
# save
# end
- def run_callbacks(kind, &block)
- send "_run_#{kind}_callbacks", &block
- end
-
- private
+ #
+ #--
+ #
+ # As this method is used in many places, and often wraps large portions of
+ # user code, it has an additional design goal of minimizing its impact on
+ # the visible call stack. An exception from inside a :before or :after
+ # callback can be as noisy as it likes -- but when control has passed
+ # smoothly through and into the supplied block, we want as little evidence
+ # as possible that we were here.
+ def run_callbacks(kind)
+ callbacks = __callbacks[kind.to_sym]
+
+ if callbacks.empty?
+ yield if block_given?
+ else
+ env = Filters::Environment.new(self, false, nil)
+ next_sequence = callbacks.compile
+
+ invoke_sequence = Proc.new do
+ skipped = nil
+ while true
+ current, next_sequence = next_sequence, next_sequence.nested
+ current.invoke_before(env)
+ if current.final?
+ env.value = !env.halted && (!block_given? || yield)
+ elsif current.skip?(env)
+ (skipped ||= []) << current
+ next
+ else
+ expanded = current.expand_call_template(env, invoke_sequence)
+ expanded.shift.send(*expanded, &expanded.shift)
+ end
+ current.invoke_after(env)
+ skipped.pop.invoke_after(env) while skipped && skipped.first
+ break env.value
+ end
+ end
- def __run_callbacks__(callbacks, &block)
- if callbacks.empty?
- yield if block_given?
+ # Common case: no 'around' callbacks defined
+ if next_sequence.final?
+ next_sequence.invoke_before(env)
+ env.value = !env.halted && (!block_given? || yield)
+ next_sequence.invoke_after(env)
+ env.value
else
- runner = callbacks.compile
- e = Filters::Environment.new(self, false, nil, block)
- runner.call(e).value
+ invoke_sequence.call
end
end
+ end
+
+ private
# A hook invoked every time a before callback is halted.
# This can be overridden in ActiveSupport::Callbacks implementors in order
@@ -118,16 +156,7 @@ module ActiveSupport
end
module Filters
- Environment = Struct.new(:target, :halted, :value, :run_block)
-
- class End
- def call(env)
- block = env.run_block
- env.value = !env.halted && (!block || block.call)
- env
- end
- end
- ENDING = End.new
+ Environment = Struct.new(:target, :halted, :value)
class Before
def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
@@ -246,51 +275,6 @@ module ActiveSupport
end
private_class_method :simple
end
-
- class Around
- def self.build(callback_sequence, user_callback, user_conditions, chain_config)
- if user_conditions.any?
- halting_and_conditional(callback_sequence, user_callback, user_conditions)
- else
- halting(callback_sequence, user_callback)
- end
- end
-
- def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
- callback_sequence.around do |env, &run|
- target = env.target
- value = env.value
- halted = env.halted
-
- if !halted && user_conditions.all? { |c| c.call(target, value) }
- user_callback.call(target, value) {
- run.call.value
- }
- env
- else
- run.call
- end
- end
- end
- private_class_method :halting_and_conditional
-
- def self.halting(callback_sequence, user_callback)
- callback_sequence.around do |env, &run|
- target = env.target
- value = env.value
-
- if env.halted
- run.call
- else
- user_callback.call(target, value) {
- run.call.value
- }
- env
- end
- end
- end
- private_class_method :halting
- end
end
class Callback #:nodoc:#
@@ -349,64 +333,23 @@ module ActiveSupport
# Wraps code with filter
def apply(callback_sequence)
user_conditions = conditions_lambdas
- user_callback = make_lambda @filter
+ user_callback = CallTemplate.build(@filter, self)
case kind
when :before
- Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter)
+ Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter)
when :after
- Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config)
+ Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
when :around
- Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config)
+ callback_sequence.around(user_callback, user_conditions)
end
end
- private
-
- def invert_lambda(l)
- lambda { |*args, &blk| !l.call(*args, &blk) }
- end
-
- # Filters support:
- #
- # Symbols:: A method to call.
- # Strings:: Some content to evaluate.
- # Procs:: A proc to call with the object.
- # Objects:: An object with a <tt>before_foo</tt> method on it to call.
- #
- # All of these objects are converted into a lambda and handled
- # the same after this point.
- def make_lambda(filter)
- case filter
- when Symbol
- lambda { |target, _, &blk| target.send filter, &blk }
- when String
- l = eval "lambda { |value| #{filter} }"
- lambda { |target, value| target.instance_exec(value, &l) }
- when Conditionals::Value then filter
- when ::Proc
- if filter.arity > 1
- return lambda { |target, _, &block|
- raise ArgumentError unless block
- target.instance_exec(target, block, &filter)
- }
- end
-
- if filter.arity <= 0
- lambda { |target, _| target.instance_exec(&filter) }
- else
- lambda { |target, _| target.instance_exec(target, &filter) }
- end
- else
- scopes = Array(chain_config[:scope])
- method_to_call = scopes.map { |s| public_send(s) }.join("_")
-
- lambda { |target, _, &blk|
- filter.public_send method_to_call, target, &blk
- }
- end
- end
+ def current_scopes
+ Array(chain_config[:scope]).map { |s| public_send(s) }
+ end
+ private
def compute_identifier(filter)
case filter
when String, ::Proc
@@ -417,17 +360,116 @@ module ActiveSupport
end
def conditions_lambdas
- @if.map { |c| make_lambda c } +
- @unless.map { |c| invert_lambda make_lambda c }
+ @if.map { |c| CallTemplate.build(c, self).make_lambda } +
+ @unless.map { |c| CallTemplate.build(c, self).inverted_lambda }
end
end
+ # A future invocation of user-supplied code (either as a callback,
+ # or a condition filter).
+ class CallTemplate # :nodoc:
+ def initialize(target, method, arguments, block)
+ @override_target = target
+ @method_name = method
+ @arguments = arguments
+ @override_block = block
+ end
+
+ # Return the parts needed to make this call, with the given
+ # input values.
+ #
+ # Returns an array of the form:
+ #
+ # [target, block, method, *arguments]
+ #
+ # This array can be used as such:
+ #
+ # target.send(method, *arguments, &block)
+ #
+ # The actual invocation is left up to the caller to minimize
+ # call stack pollution.
+ def expand(target, value, block)
+ result = @arguments.map { |arg|
+ case arg
+ when :value; value
+ when :target; target
+ when :block; block || raise(ArgumentError)
+ end
+ }
+
+ result.unshift @method_name
+ result.unshift @override_block || block
+ result.unshift @override_target || target
+
+ # target, block, method, *arguments = result
+ # target.send(method, *arguments, &block)
+ result
+ end
+
+ # Return a lambda that will make this call when given the input
+ # values.
+ def make_lambda
+ lambda do |target, value, &block|
+ c = expand(target, value, block)
+ c.shift.send(*c, &c.shift)
+ end
+ end
+
+ # Return a lambda that will make this call when given the input
+ # values, but then return the boolean inverse of that result.
+ def inverted_lambda
+ lambda do |target, value, &block|
+ c = expand(target, value, block)
+ ! c.shift.send(*c, &c.shift)
+ end
+ end
+
+ # Filters support:
+ #
+ # Symbols:: A method to call.
+ # Strings:: Some content to evaluate.
+ # Procs:: A proc to call with the object.
+ # Objects:: An object with a <tt>before_foo</tt> method on it to call.
+ #
+ # All of these objects are converted into a CallTemplate and handled
+ # the same after this point.
+ def self.build(filter, callback)
+ case filter
+ when Symbol
+ new(nil, filter, [], nil)
+ when String
+ new(nil, :instance_exec, [:value], compile_lambda(filter))
+ when Conditionals::Value
+ new(filter, :call, [:target, :value], nil)
+ when ::Proc
+ if filter.arity > 1
+ new(nil, :instance_exec, [:target, :block], filter)
+ elsif filter.arity > 0
+ new(nil, :instance_exec, [:target], filter)
+ else
+ new(nil, :instance_exec, [], filter)
+ end
+ else
+ method_to_call = callback.current_scopes.join("_")
+
+ new(filter, method_to_call, [:target], nil)
+ end
+ end
+
+ def self.compile_lambda(filter)
+ eval("lambda { |value| #{filter} }")
+ end
+ end
+
# Execute before and after filters in a sequence instead of
# chaining them with nested lambda calls, see:
# https://github.com/rails/rails/issues/18011
- class CallbackSequence
- def initialize(&call)
- @call = call
+ class CallbackSequence # :nodoc:
+ def initialize(nested = nil, call_template = nil, user_conditions = nil)
+ @nested = nested
+ @call_template = call_template
+ @user_conditions = user_conditions
+
@before = []
@after = []
end
@@ -442,19 +484,32 @@ module ActiveSupport
self
end
- def around(&around)
- CallbackSequence.new do |arg|
- around.call(arg) {
- call(arg)
- }
- end
+ def around(call_template, user_conditions)
+ CallbackSequence.new(self, call_template, user_conditions)
+ end
+
+ def skip?(arg)
+ arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) }
+ end
+
+ def nested
+ @nested
end
- def call(arg)
+ def final?
+ !@call_template
+ end
+
+ def expand_call_template(arg, block)
+ @call_template.expand(arg.target, arg.value, block)
+ end
+
+ def invoke_before(arg)
@before.each { |b| b.call(arg) }
- value = @call.call(arg)
+ end
+
+ def invoke_after(arg)
@after.each { |a| a.call(arg) }
- value
end
end
@@ -503,7 +558,7 @@ module ActiveSupport
def compile
@callbacks || @mutex.synchronize do
- final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) }
+ final_sequence = CallbackSequence.new
@callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
callback.apply callback_sequence
end
@@ -747,12 +802,25 @@ module ActiveSupport
options = names.extract_options!
names.each do |name|
- class_attribute "_#{name}_callbacks", instance_writer: false
+ name = name.to_sym
+
set_callbacks name, CallbackChain.new(name, options)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
- __run_callbacks__(_#{name}_callbacks, &block)
+ run_callbacks #{name.inspect}, &block
+ end
+
+ def self._#{name}_callbacks
+ get_callbacks(#{name.inspect})
+ end
+
+ def self._#{name}_callbacks=(value)
+ set_callbacks(#{name.inspect}, value)
+ end
+
+ def _#{name}_callbacks
+ __callbacks[#{name.inspect}]
end
RUBY
end
@@ -761,11 +829,11 @@ module ActiveSupport
protected
def get_callbacks(name) # :nodoc:
- send "_#{name}_callbacks"
+ __callbacks[name.to_sym]
end
def set_callbacks(name, callbacks) # :nodoc:
- send "_#{name}_callbacks=", callbacks
+ self.__callbacks = __callbacks.merge(name.to_sym => callbacks)
end
def deprecated_false_terminator # :nodoc:
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index 9b4a9a9992..ba422f9071 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -20,7 +20,7 @@ class Class
# Base.setting # => true
#
# In the above case as long as Subclass does not assign a value to setting
- # by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
+ # by performing <tt>Subclass.setting = _something_</tt>, <tt>Subclass.setting</tt>
# would read value assigned to parent class. Once Subclass assigns a value then
# the value assigned by Subclass would be returned.
#
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index 792076a449..c614f14289 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -300,7 +300,7 @@ module DateAndTime
end
# Returns a Range representing the whole week of the current date/time.
- # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set.
+ # Week starts on start_day, default is <tt>Date.beginning_of_week</tt> or <tt>config.beginning_of_week</tt> when set.
def all_week(start_day = Date.beginning_of_week)
beginning_of_week(start_day)..end_of_week(start_day)
end
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
index 3f4e236ab7..db95ae0db5 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
@@ -12,7 +12,11 @@ module DateAndTime
mattr_accessor(:preserve_timezone, instance_writer: false) { false }
def to_time
- preserve_timezone ? getlocal(utc_offset) : getlocal
+ if preserve_timezone
+ @_to_time_with_instance_offset ||= getlocal(utc_offset)
+ else
+ @_to_time_with_system_offset ||= getlocal
+ end
end
end
end
diff --git a/activesupport/lib/active_support/deprecation/instance_delegator.rb b/activesupport/lib/active_support/deprecation/instance_delegator.rb
index 8efa6aabdc..6d390f3b37 100644
--- a/activesupport/lib/active_support/deprecation/instance_delegator.rb
+++ b/activesupport/lib/active_support/deprecation/instance_delegator.rb
@@ -6,6 +6,7 @@ module ActiveSupport
module InstanceDelegator # :nodoc:
def self.included(base)
base.extend(ClassMethods)
+ base.singleton_class.prepend(OverrideDelegators)
base.public_class_method :new
end
@@ -19,6 +20,18 @@ module ActiveSupport
singleton_class.delegate(method_name, to: :instance)
end
end
+
+ module OverrideDelegators # :nodoc:
+ def warn(message = nil, callstack = nil)
+ callstack ||= caller_locations(2)
+ super
+ end
+
+ def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
+ caller_backtrace ||= caller_locations(2)
+ super
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb
index 4c8b03c9df..3384d12d5b 100644
--- a/activesupport/lib/active_support/execution_wrapper.rb
+++ b/activesupport/lib/active_support/execution_wrapper.rb
@@ -19,6 +19,23 @@ module ActiveSupport
set_callback(:complete, *args, &block)
end
+ class RunHook < Struct.new(:hook) # :nodoc:
+ def before(target)
+ hook_state = target.send(:hook_state)
+ hook_state[hook] = hook.run
+ end
+ end
+
+ class CompleteHook < Struct.new(:hook) # :nodoc:
+ def before(target)
+ hook_state = target.send(:hook_state)
+ if hook_state.key?(hook)
+ hook.complete hook_state[hook]
+ end
+ end
+ alias after before
+ end
+
# Register an object to be invoked during both the +run+ and
# +complete+ steps.
#
@@ -29,19 +46,11 @@ module ActiveSupport
# invoked in that situation.)
def self.register_hook(hook, outer: false)
if outer
- run_args = [prepend: true]
- complete_args = [:after]
+ to_run RunHook.new(hook), prepend: true
+ to_complete :after, CompleteHook.new(hook)
else
- run_args = complete_args = []
- end
-
- to_run(*run_args) do
- hook_state[hook] = hook.run
- end
- to_complete(*complete_args) do
- if hook_state.key?(hook)
- hook.complete hook_state[hook]
- end
+ to_run RunHook.new(hook)
+ to_complete CompleteHook.new(hook)
end
end
diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb
index b84c7253a0..ae1897b886 100644
--- a/activesupport/lib/active_support/lazy_load_hooks.rb
+++ b/activesupport/lib/active_support/lazy_load_hooks.rb
@@ -15,9 +15,9 @@ module ActiveSupport
# end
# end
#
- # When the entirety of +activerecord/lib/active_record/base.rb+ has been
+ # When the entirety of +ActiveRecord::Base+ has been
# evaluated then +run_load_hooks+ is invoked. The very last line of
- # +activerecord/lib/active_record/base.rb+ is:
+ # +ActiveRecord::Base+ is:
#
# ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
module LazyLoadHooks
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index 2159abef14..217919ccb8 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -30,36 +30,6 @@ module ActiveSupport
HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT
HANGUL_SCOUNT = 11172
HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT
- HANGUL_JAMO_FIRST = 0x1100
- HANGUL_JAMO_LAST = 0x11FF
-
- # All the unicode whitespace
- WHITESPACE = [
- (0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D>
- 0x0020, # White_Space # Zs SPACE
- 0x0085, # White_Space # Cc <control-0085>
- 0x00A0, # White_Space # Zs NO-BREAK SPACE
- 0x1680, # White_Space # Zs OGHAM SPACE MARK
- (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE
- 0x2028, # White_Space # Zl LINE SEPARATOR
- 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR
- 0x202F, # White_Space # Zs NARROW NO-BREAK SPACE
- 0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE
- 0x3000, # White_Space # Zs IDEOGRAPHIC SPACE
- ].flatten.freeze
-
- # BOM (byte order mark) can also be seen as whitespace, it's a
- # non-rendering character used to distinguish between little and big
- # endian. This is not an issue in utf-8, so it must be ignored.
- LEADERS_AND_TRAILERS = WHITESPACE + [65279] # ZERO-WIDTH NO-BREAK SPACE aka BOM
-
- # Returns a regular expression pattern that matches the passed Unicode
- # codepoints.
- def self.codepoints_to_pattern(array_of_codepoints) #:nodoc:
- array_of_codepoints.collect { |e| [e].pack "U*".freeze }.join("|".freeze)
- end
- TRAILERS_PAT = /(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+\Z/u
- LEADERS_PAT = /\A(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+/u
# Detect whether the codepoint is in a certain character class. Returns
# +true+ when it's in the specified character class and +false+ otherwise.
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index c35588fbae..889f71c4f3 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -80,7 +80,7 @@ module ActiveSupport
# Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
def localtime(utc_offset = nil)
- @localtime ||= utc.getlocal(utc_offset)
+ utc.getlocal(utc_offset)
end
alias_method :getlocal, :localtime
@@ -477,6 +477,8 @@ module ActiveSupport
end
def transfer_time_values_to_utc_constructor(time)
+ # avoid creating another Time object if possible
+ return time if time.instance_of?(::Time) && time.utc?
::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + time.subsec)
end
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index b4e98edd84..783952c8c7 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -56,6 +56,8 @@ module CallbacksTest
end
class Person < Record
+ attr_accessor :save_fails
+
[:before_save, :after_save].each do |callback_method|
callback_method_sym = callback_method.to_sym
send(callback_method, callback_symbol(callback_method_sym))
@@ -67,7 +69,9 @@ module CallbacksTest
end
def save
- run_callbacks :save
+ run_callbacks :save do
+ raise "inside save" if save_fails
+ end
end
end
@@ -222,6 +226,7 @@ module CallbacksTest
class AroundPerson < MySuper
attr_reader :history
+ attr_accessor :save_fails
set_callback :save, :before, :nope, if: :no
set_callback :save, :before, :nope, unless: :yes
@@ -285,6 +290,7 @@ module CallbacksTest
def save
run_callbacks :save do
+ raise "inside save" if save_fails
@history << "running"
end
end
@@ -402,6 +408,71 @@ module CallbacksTest
end
end
+ class CallStackTest < ActiveSupport::TestCase
+ def test_tidy_call_stack
+ around = AroundPerson.new
+ around.save_fails = true
+
+ exception = (around.save rescue $!)
+
+ # Make sure we have the exception we're expecting
+ assert_equal "inside save", exception.message
+
+ call_stack = exception.backtrace_locations
+ call_stack.pop caller_locations(0).size
+
+ # Yes, this looks like an implementation test, but it's the least
+ # obtuse way of asserting that there aren't a load of entries in
+ # the call stack for each callback.
+ #
+ # If you've renamed a method, or squeezed more lines out, go ahead
+ # and update this assertion. But if you're here because a
+ # refactoring added new lines, please reconsider.
+
+ # As shown here, our current budget is one line for run_callbacks
+ # itself, plus N+1 lines where N is the number of :around
+ # callbacks that have been invoked, if there are any (plus
+ # whatever the callbacks do themselves, of course).
+
+ assert_equal [
+ "block in save",
+ "block in run_callbacks",
+ "tweedle_deedle",
+ "block in run_callbacks",
+ "w0tyes",
+ "block in run_callbacks",
+ "tweedle_dum",
+ "block in run_callbacks",
+ ("call" if RUBY_VERSION < "2.3"),
+ "run_callbacks",
+ "save"
+ ].compact, call_stack.map(&:label)
+ end
+
+ def test_short_call_stack
+ person = Person.new
+ person.save_fails = true
+
+ exception = (person.save rescue $!)
+
+ # Make sure we have the exception we're expecting
+ assert_equal "inside save", exception.message
+
+ call_stack = exception.backtrace_locations
+ call_stack.pop caller_locations(0).size
+
+ # This budget much simpler: with no :around callbacks invoked,
+ # there should be just one line. run_callbacks yields directly
+ # back to its caller.
+
+ assert_equal [
+ "block in save",
+ "run_callbacks",
+ "save"
+ ], call_stack.map(&:label)
+ end
+ end
+
class AroundCallbackResultTest < ActiveSupport::TestCase
def test_save_around
around = AroundPersonResult.new
diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb
index 0b56ea008f..d409216206 100644
--- a/activesupport/test/executor_test.rb
+++ b/activesupport/test/executor_test.rb
@@ -158,6 +158,61 @@ class ExecutorTest < ActiveSupport::TestCase
assert_equal :some_state, supplied_state
end
+ def test_hook_insertion_order
+ invoked = []
+ supplied_state = []
+
+ hook_class = Class.new do
+ attr_accessor :letter
+
+ define_method(:initialize) do |letter|
+ self.letter = letter
+ end
+
+ define_method(:run) do
+ invoked << :"run_#{letter}"
+ :"state_#{letter}"
+ end
+
+ define_method(:complete) do |state|
+ invoked << :"complete_#{letter}"
+ supplied_state << state
+ end
+ end
+
+ executor.register_hook(hook_class.new(:a))
+ executor.register_hook(hook_class.new(:b))
+ executor.register_hook(hook_class.new(:c), outer: true)
+ executor.register_hook(hook_class.new(:d))
+
+ executor.wrap {}
+
+ assert_equal [:run_c, :run_a, :run_b, :run_d, :complete_a, :complete_b, :complete_d, :complete_c], invoked
+ assert_equal [:state_a, :state_b, :state_d, :state_c], supplied_state
+ end
+
+ def test_class_serial_is_unaffected
+ hook = Class.new do
+ define_method(:run) do
+ nil
+ end
+
+ define_method(:complete) do |state|
+ nil
+ end
+ end.new
+
+ executor.register_hook(hook)
+
+ before = RubyVM.stat(:class_serial)
+ executor.wrap {}
+ executor.wrap {}
+ executor.wrap {}
+ after = RubyVM.stat(:class_serial)
+
+ assert_equal before, after
+ end
+
def test_separate_classes_can_wrap
other_executor = Class.new(ActiveSupport::Executor)
diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md
index c6bac34d18..ac5833e069 100644
--- a/guides/source/2_2_release_notes.md
+++ b/guides/source/2_2_release_notes.md
@@ -45,7 +45,6 @@ The internal documentation of Rails, in the form of code comments, has been impr
* [A Guide to Testing Rails Applications](testing.html)
* [Securing Rails Applications](security.html)
* [Debugging Rails Applications](debugging_rails_applications.html)
-* [Performance Testing Rails Applications](performance_testing.html)
* [The Basics of Creating Rails Plugins](plugins.html)
All told, the Guides provide tens of thousands of words of guidance for beginning and intermediate Rails developers.
diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md
index 50886a57a7..cc332cbf97 100644
--- a/guides/source/5_0_release_notes.md
+++ b/guides/source/5_0_release_notes.md
@@ -583,7 +583,7 @@ Please refer to the [Changelog][active-record] for detailed changes.
* Removed support for the legacy `mysql` database adapter from core. Most users should
be able to use `mysql2`. It will be converted to a separate gem when when we find someone
- to maintain it. ([Pull Request 1](https://github.com/rails/rails/pull/22642)],
+ to maintain it. ([Pull Request 1](https://github.com/rails/rails/pull/22642),
[Pull Request 2](https://github.com/rails/rails/pull/22715))
* Removed support for the `protected_attributes` gem.
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
index 4b9a22101a..a66a8ff484 100644
--- a/guides/source/action_cable_overview.md
+++ b/guides/source/action_cable_overview.md
@@ -422,7 +422,7 @@ App.cable.subscriptions.create "AppearanceChannel",
buttonSelector = "[data-behavior~=appear_away]"
install: ->
- $(document).on "page:change.appearance", =>
+ $(document).on "turbolinks:load.appearance", =>
@appear()
$(document).on "click.appearance", buttonSelector, =>
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index bbe1b0decc..38b1ffc4c8 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -81,7 +81,6 @@ The methods are:
* `reorder`
* `reverse_order`
* `select`
-* `distinct`
* `where`
Finder methods that return a collection, such as `where` and `group`, return an instance of `ActiveRecord::Relation`. Methods that find a single entity, such as `find` and `first`, return a single instance of the model.
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index 03af3cf819..3fc9d9bfa9 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -231,12 +231,13 @@ Active Record
### sql.active_record
-| Key | Value |
-| ---------------- | --------------------- |
-| `:sql` | SQL statement |
-| `:name` | Name of the operation |
-| `:connection_id` | `self.object_id` |
-| `:binds` | Bind parameters |
+| Key | Value |
+| ---------------- | ---------------------------------------- |
+| `:sql` | SQL statement |
+| `:name` | Name of the operation |
+| `:connection_id` | `self.object_id` |
+| `:binds` | Bind parameters |
+| `:cached` | `true` is added when cached queries used |
INFO. The adapters will add their own data as well.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index fbf3c27957..3df8a0ed26 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -1296,7 +1296,7 @@ evented file system monitor to detect changes when `config.cache_classes` is
```ruby
group :development do
- gem 'listen', '~> 3.0.4'
+ gem 'listen', '>= 3.0.5', '< 3.2'
end
```
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 98847fde18..0ac5121b12 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -1317,8 +1317,8 @@ end
This test is pretty simple and only asserts that the job get the work done
as expected.
-By default, `ActiveJob::TestCase` will set the queue adapter to `:async` so that
-your jobs are performed in an async fashion. It will also ensure that all previously performed
+By default, `ActiveJob::TestCase` will set the queue adapter to `:test` so that
+your jobs are performed inline. It will also ensure that all previously performed
and enqueued jobs are cleared before any test run so you can safely assume that
no jobs have already been executed in the scope of each test.
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index b03c87e9ba..b488e4ed8e 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Allow the use of listen's 3.1.x branch
+
+ *Esteban Santana Santana*
+
* Run `Minitest.after_run` hooks when running `rails test`.
*Michael Grosser*
diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb
index 4349dfdc71..14cf72f483 100644
--- a/railties/lib/rails/commands/server/server_command.rb
+++ b/railties/lib/rails/commands/server/server_command.rb
@@ -21,7 +21,7 @@ module Rails
def option_parser(options) # :nodoc:
OptionParser.new do |opts|
- opts.banner = "Usage: rails server [mongrel, thin etc] [options]"
+ opts.banner = "Usage: rails server [puma, thin etc] [options]"
opts.separator ""
opts.separator "Options:"
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index 7af5fcd3c1..422217286c 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -39,7 +39,7 @@ group :development do
<%- end -%>
<%- end -%>
<% if depend_on_listen? -%>
- gem 'listen', '~> 3.0.5'
+ gem 'listen', '>= 3.0.5', '< 3.2'
<% end -%>
<% if spring_install? -%>
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
index 62b94618fd..c0fbb84a93 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
@@ -5,4 +5,6 @@ require 'rails/test_unit/minitest_plugin'
Rails::TestUnitReporter.executable = 'bin/test'
-exit Minitest.run(ARGV)
+Minitest.run_via[:rails] = true
+
+require "active_support/testing/autorun"
diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb
index 04b4b10254..7a10a2afa9 100644
--- a/railties/test/generators/plugin_test_runner_test.rb
+++ b/railties/test/generators/plugin_test_runner_test.rb
@@ -86,6 +86,12 @@ class PluginTestRunnerTest < ActiveSupport::TestCase
assert_match(%r{cannot load such file.+test/not_exists\.rb}, error)
end
+ def test_executed_only_once
+ create_test_file "foo"
+ result = run_test_command("test/foo_test.rb")
+ assert_equal 1, result.scan(/1 runs, 1 assertions, 0 failures/).length
+ end
+
private
def plugin_path
"#{@destination_root}/bukkits"