aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--Gemfile.lock2
-rw-r--r--actioncable/app/assets/javascripts/action_cable/connection.coffee14
-rw-r--r--actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee1
-rw-r--r--actioncable/app/assets/javascripts/action_cable/consumer.coffee4
-rw-r--r--actioncable/app/assets/javascripts/action_cable/subscriptions.coffee2
-rw-r--r--actioncable/lib/action_cable/channel/periodic_timers.rb2
-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.rb4
-rw-r--r--actioncable/lib/action_cable/connection/client_socket.rb10
-rw-r--r--actioncable/lib/action_cable/connection/faye_client_socket.rb42
-rw-r--r--actioncable/lib/action_cable/connection/faye_event_loop.rb44
-rw-r--r--actioncable/lib/action_cable/connection/internal_channel.rb4
-rw-r--r--actioncable/lib/action_cable/connection/stream_event_loop.rb11
-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/connections.rb6
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/async.rb11
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/postgresql.rb9
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/redis.rb9
-rw-r--r--actioncable/test/channel/periodic_timers_test.rb2
-rw-r--r--actioncable/test/client_test.rb5
-rw-r--r--actioncable/test/connection/authorization_test.rb2
-rw-r--r--actioncable/test/connection/base_test.rb7
-rw-r--r--actioncable/test/connection/cross_site_forgery_test.rb2
-rw-r--r--actioncable/test/connection/identifier_test.rb2
-rw-r--r--actioncable/test/connection/multiple_identifiers_test.rb2
-rw-r--r--actioncable/test/connection/string_identifier_test.rb2
-rw-r--r--actioncable/test/connection/subscriptions_test.rb2
-rw-r--r--actioncable/test/stubs/test_connection.rb5
-rw-r--r--actioncable/test/stubs/test_server.rb14
-rw-r--r--actioncable/test/subscription_adapter/common.rb1
-rw-r--r--actioncable/test/test_helper.rb44
-rw-r--r--actionmailer/lib/action_mailer/inline_preview_interceptor.rb2
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb8
-rw-r--r--actionpack/lib/action_controller/test_case.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb10
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb2
-rw-r--r--actionpack/test/dispatch/ssl_test.rb8
-rw-r--r--activerecord/CHANGELOG.md8
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb6
-rw-r--r--activerecord/test/cases/associations/eager_test.rb12
-rw-r--r--activerecord/test/models/author.rb8
-rw-r--r--activesupport/lib/active_support/execution_wrapper.rb21
-rw-r--r--activesupport/lib/active_support/reloader.rb4
-rw-r--r--guides/source/caching_with_rails.md21
-rw-r--r--guides/source/layouts_and_rendering.md2
-rw-r--r--guides/source/security.md6
-rw-r--r--guides/source/testing.md2
53 files changed, 326 insertions, 109 deletions
diff --git a/.travis.yml b/.travis.yml
index ae38617b99..bd1a320de5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,6 +17,7 @@ env:
- "GEM=railties"
- "GEM=ap"
- "GEM=ac"
+ - "GEM=ac FAYE=1"
- "GEM=am,amo,as,av,aj"
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
diff --git a/Gemfile.lock b/Gemfile.lock
index 3224c31a0b..90c15b4175 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -131,7 +131,7 @@ GEM
erubis (2.7.0)
eventmachine (1.0.9.1)
execjs (2.6.0)
- faye-websocket (0.10.2)
+ faye-websocket (0.10.3)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.9.10)
diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee
index e8c9ae6bd0..9be5cdf5fb 100644
--- a/actioncable/app/assets/javascripts/action_cable/connection.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee
@@ -8,9 +8,6 @@ class ActionCable.Connection
constructor: (@consumer) ->
send: (data) ->
- unless @isOpen()
- @open()
-
if @isOpen()
@webSocket.send(JSON.stringify(data))
true
@@ -18,7 +15,7 @@ class ActionCable.Connection
false
open: =>
- if @isAlive()
+ if @isActive()
ActionCable.log("Attemped to open WebSocket, but existing socket is #{@getState()}")
throw new Error("Existing connection must be closed before opening")
else
@@ -33,7 +30,7 @@ class ActionCable.Connection
reopen: ->
ActionCable.log("Reopening WebSocket, current state is #{@getState()}")
- if @isAlive()
+ if @isActive()
try
@close()
catch error
@@ -47,10 +44,10 @@ class ActionCable.Connection
isOpen: ->
@isState("open")
- # Private
+ isActive: ->
+ @isState("open", "connecting")
- isAlive: ->
- @webSocket? and not @isState("closing", "closed")
+ # Private
isState: (states...) ->
@getState() in states
@@ -101,4 +98,5 @@ class ActionCable.Connection
disconnect: ->
return if @disconnected
@disconnected = true
+ @consumer.connectionMonitor.disconnected()
@consumer.subscriptions.notifyAll("disconnected")
diff --git a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee
index 740e86643e..904a426644 100644
--- a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee
@@ -18,6 +18,7 @@ class ActionCable.ConnectionMonitor
disconnected: ->
@disconnectedAt = now()
+ ActionCable.log("ConnectionMonitor disconnected")
ping: ->
@pingedAt = now()
diff --git a/actioncable/app/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee
index 717c0641a9..3d93d40b99 100644
--- a/actioncable/app/assets/javascripts/action_cable/consumer.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/consumer.coffee
@@ -23,3 +23,7 @@ class ActionCable.Consumer
send: (data) ->
@connection.send(data)
+
+ ensureActiveConnection: ->
+ unless @connection.isActive()
+ @connection.open()
diff --git a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee
index 2443bca14a..9d93b462a7 100644
--- a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee
@@ -19,6 +19,7 @@ class ActionCable.Subscriptions
add: (subscription) ->
@subscriptions.push(subscription)
+ @consumer.ensureActiveConnection()
@notify(subscription, "initialized")
@sendCommand(subscription, "subscribe")
@@ -59,4 +60,3 @@ class ActionCable.Subscriptions
sendCommand: (subscription, command) ->
{identifier} = subscription
@consumer.send({command, identifier})
-
diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb
index 0f6e854520..b414255707 100644
--- a/actioncable/lib/action_cable/channel/periodic_timers.rb
+++ b/actioncable/lib/action_cable/channel/periodic_timers.rb
@@ -27,7 +27,7 @@ module ActionCable
def start_periodic_timers
self.class.periodic_timers.each do |callback, options|
- active_periodic_timers << Concurrent::TimerTask.new(execution_interval: options[:every]) do
+ active_periodic_timers << connection.server.event_loop.timer(options[:every]) do
connection.worker_pool.async_run_periodic_timer(self, callback)
end
end
diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb
index 431a5c1063..23d7320a28 100644
--- a/actioncable/lib/action_cable/channel/streams.rb
+++ b/actioncable/lib/action_cable/channel/streams.rb
@@ -79,7 +79,7 @@ module ActionCable
callback ||= default_stream_callback(broadcasting)
streams << [ broadcasting, callback ]
- Concurrent.global_io_executor.post do
+ connection.server.event_loop.post do
pubsub.subscribe(broadcasting, callback, lambda do
transmit_subscription_confirmation
logger.info "#{self.class.name} is streaming from #{broadcasting}"
diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb
index 902efb07e2..5f813cf8e0 100644
--- a/actioncable/lib/action_cable/connection.rb
+++ b/actioncable/lib/action_cable/connection.rb
@@ -8,6 +8,8 @@ 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 f34f5eb109..b4488265cb 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -49,7 +49,7 @@ module ActionCable
include Authorization
attr_reader :server, :env, :subscriptions, :logger, :worker_pool
- delegate :stream_event_loop, :pubsub, to: :server
+ delegate :event_loop, :pubsub, to: :server
def initialize(server, env)
@server, @env = server, env
@@ -57,7 +57,7 @@ module ActionCable
@worker_pool = server.worker_pool
@logger = new_tagged_logger
- @websocket = ActionCable::Connection::WebSocket.new(env, self, stream_event_loop)
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class)
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb
index f6b11e93f0..9e4dbcd6e6 100644
--- a/actioncable/lib/action_cable/connection/client_socket.rb
+++ b/actioncable/lib/action_cable/connection/client_socket.rb
@@ -29,10 +29,10 @@ module ActionCable
attr_reader :env, :url
- def initialize(env, event_target, stream_event_loop)
- @env = env
- @event_target = event_target
- @stream_event_loop = stream_event_loop
+ def initialize(env, event_target, event_loop)
+ @env = env
+ @event_target = event_target
+ @event_loop = event_loop
@url = ClientSocket.determine_url(@env)
@@ -49,7 +49,7 @@ module ActionCable
@driver.on(:close) { |e| begin_close(e.reason, e.code) }
@driver.on(:error) { |e| emit_error(e.message) }
- @stream = ActionCable::Connection::Stream.new(@stream_event_loop, self)
+ @stream = ActionCable::Connection::Stream.new(@event_loop, self)
end
def start_driver
diff --git a/actioncable/lib/action_cable/connection/faye_client_socket.rb b/actioncable/lib/action_cable/connection/faye_client_socket.rb
new file mode 100644
index 0000000000..c9139b6858
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/faye_client_socket.rb
@@ -0,0 +1,42 @@
+require 'faye/websocket'
+
+module ActionCable
+ module Connection
+ class FayeClientSocket
+ def initialize(env, event_target, stream_event_loop)
+ @env = env
+ @event_target = event_target
+
+ @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 rack_response
+ connect
+ @faye.rack_response
+ end
+
+ private
+ def connect
+ return if @faye
+ @faye = Faye::WebSocket.new(@env)
+
+ @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) }
+ 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
new file mode 100644
index 0000000000..8b70f3d84e
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/faye_event_loop.rb
@@ -0,0 +1,44 @@
+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/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb
index 27826792b3..3c5d39f59a 100644
--- a/actioncable/lib/action_cable/connection/internal_channel.rb
+++ b/actioncable/lib/action_cable/connection/internal_channel.rb
@@ -15,14 +15,14 @@ module ActionCable
@_internal_subscriptions ||= []
@_internal_subscriptions << [ internal_channel, callback ]
- Concurrent.global_io_executor.post { pubsub.subscribe(internal_channel, callback) }
+ server.event_loop.post { pubsub.subscribe(internal_channel, callback) }
logger.info "Registered connection (#{connection_identifier})"
end
end
def unsubscribe_from_internal_channel
if @_internal_subscriptions.present?
- @_internal_subscriptions.each { |channel, callback| Concurrent.global_io_executor.post { pubsub.unsubscribe(channel, callback) } }
+ @_internal_subscriptions.each { |channel, callback| server.event_loop.post { pubsub.unsubscribe(channel, callback) } }
end
end
diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb
index e6335082d2..2abad09c03 100644
--- a/actioncable/lib/action_cable/connection/stream_event_loop.rb
+++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb
@@ -11,7 +11,16 @@ module ActionCable
@todo = Queue.new
@spawn_mutex = Mutex.new
- spawn
+ end
+
+ def timer(interval, &block)
+ Concurrent::TimerTask.new(execution_interval: interval, &block).tap(&:execute)
+ end
+
+ def post(task = nil, &block)
+ task ||= block
+
+ Concurrent.global_io_executor << task
end
def attach(io, stream)
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 5e89fb9b72..0bec9b6a96 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, stream_event_loop)
- @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, stream_event_loop) : nil
+ def initialize(env, event_target, event_loop, client_socket_class)
+ @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop) : nil
end
def possible?
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
index d9a2653cc2..778f5ffeed 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -1,4 +1,4 @@
-require 'thread'
+require 'monitor'
module ActionCable
module Server
@@ -18,8 +18,8 @@ module ActionCable
attr_reader :mutex
def initialize
- @mutex = Mutex.new
- @remote_connections = @stream_event_loop = @worker_pool = @channel_classes = @pubsub = nil
+ @mutex = Monitor.new
+ @remote_connections = @event_loop = @worker_pool = @channel_classes = @pubsub = nil
end
# Called by Rack to setup the server.
@@ -48,8 +48,8 @@ module ActionCable
@remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
end
- def stream_event_loop
- @stream_event_loop || @mutex.synchronize { @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new }
+ def event_loop
+ @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
end
# The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size.
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
index 9a7301287c..5fe71caed2 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 :connection_class, :worker_pool_size
+ attr_accessor :use_faye, :connection_class, :worker_pool_size
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
attr_accessor :cable, :url, :mount_path
@@ -43,6 +43,22 @@ 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/connections.rb b/actioncable/lib/action_cable/server/connections.rb
index 4dc8934b25..5e61b4e335 100644
--- a/actioncable/lib/action_cable/server/connections.rb
+++ b/actioncable/lib/action_cable/server/connections.rb
@@ -21,9 +21,9 @@ module ActionCable
# then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
# disconnect.
def setup_heartbeat_timer
- @heartbeat_timer ||= Concurrent::TimerTask.new(execution_interval: BEAT_INTERVAL) do
- Concurrent.global_io_executor.post { connections.map(&:beat) }
- end.tap(&:execute)
+ @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
+ event_loop.post { connections.map(&:beat) }
+ end
end
def open_connections_statistics
diff --git a/actioncable/lib/action_cable/subscription_adapter/async.rb b/actioncable/lib/action_cable/subscription_adapter/async.rb
index cca6894289..10b3ac8cd8 100644
--- a/actioncable/lib/action_cable/subscription_adapter/async.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/async.rb
@@ -5,16 +5,21 @@ module ActionCable
class Async < Inline # :nodoc:
private
def new_subscriber_map
- AsyncSubscriberMap.new
+ AsyncSubscriberMap.new(server.event_loop)
end
class AsyncSubscriberMap < SubscriberMap
+ def initialize(event_loop)
+ @event_loop = event_loop
+ super()
+ end
+
def add_subscriber(*)
- Concurrent.global_io_executor.post { super }
+ @event_loop.post { super }
end
def invoke_callback(*)
- Concurrent.global_io_executor.post { super }
+ @event_loop.post { super }
end
end
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
index abaeb92e54..66c7852f6e 100644
--- a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
@@ -42,14 +42,15 @@ module ActionCable
private
def listener
- @listener || @server.mutex.synchronize { @listener ||= Listener.new(self) }
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
end
class Listener < SubscriberMap
- def initialize(adapter)
+ def initialize(adapter, event_loop)
super()
@adapter = adapter
+ @event_loop = event_loop
@queue = Queue.new
@thread = Thread.new do
@@ -68,7 +69,7 @@ module ActionCable
case action
when :listen
pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}")
- Concurrent.global_io_executor << callback if callback
+ @event_loop.post(&callback) if callback
when :unlisten
pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}")
when :shutdown
@@ -98,7 +99,7 @@ module ActionCable
end
def invoke_callback(*)
- Concurrent.global_io_executor.post { super }
+ @event_loop.post { super }
end
end
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb
index 6b4236e7d3..65434f7107 100644
--- a/actioncable/lib/action_cable/subscription_adapter/redis.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb
@@ -38,7 +38,7 @@ module ActionCable
private
def listener
- @listener || @server.mutex.synchronize { @listener ||= Listener.new(self) }
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
end
def redis_connection_for_broadcasts
@@ -52,10 +52,11 @@ module ActionCable
end
class Listener < SubscriberMap
- def initialize(adapter)
+ def initialize(adapter, event_loop)
super()
@adapter = adapter
+ @event_loop = event_loop
@subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
@subscription_lock = Mutex.new
@@ -84,7 +85,7 @@ module ActionCable
if callbacks = @subscribe_callbacks[chan]
next_callback = callbacks.shift
- Concurrent.global_io_executor << next_callback if next_callback
+ @event_loop.post(&next_callback) if next_callback
@subscribe_callbacks.delete(chan) if callbacks.empty?
end
end
@@ -133,7 +134,7 @@ module ActionCable
end
def invoke_callback(*)
- Concurrent.global_io_executor.post { super }
+ @event_loop.post { super }
end
private
diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb
index 64f0247cd6..e6f0c14c9d 100644
--- a/actioncable/test/channel/periodic_timers_test.rb
+++ b/actioncable/test/channel/periodic_timers_test.rb
@@ -31,7 +31,7 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase
end
test "timer start and stop" do
- Concurrent::TimerTask.expects(:new).times(2).returns(true)
+ @connection.server.event_loop.expects(:timer).times(2).returns(true)
channel = ChatChannel.new @connection, "{id: 1}", { id: 1 }
channel.expects(:stop_periodic_timers).once
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
index 75545993da..5f5c09d1a1 100644
--- a/actioncable/test/client_test.rb
+++ b/actioncable/test/client_test.rb
@@ -8,8 +8,8 @@ require 'faye/websocket'
require 'json'
class ClientTest < ActionCable::TestCase
- WAIT_WHEN_EXPECTING_EVENT = 3
- WAIT_WHEN_NOT_EXPECTING_EVENT = 0.2
+ WAIT_WHEN_EXPECTING_EVENT = 8
+ WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5
def setup
ActionCable.instance_variable_set(:@server, nil)
@@ -17,6 +17,7 @@ class ClientTest < ActionCable::TestCase
server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
server.config.cable = { adapter: 'async' }.with_indifferent_access
+ server.config.use_faye = ENV['FAYE'].present?
# and now the "real" setup for our test:
server.config.disable_request_forgery_protection = true
diff --git a/actioncable/test/connection/authorization_test.rb b/actioncable/test/connection/authorization_test.rb
index 87d0e79ef3..a0506cb9c0 100644
--- a/actioncable/test/connection/authorization_test.rb
+++ b/actioncable/test/connection/authorization_test.rb
@@ -20,7 +20,7 @@ class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase
server.config.allowed_request_origins = %w( http://rubyonrails.com )
env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket',
- 'HTTP_ORIGIN' => 'http://rubyonrails.com'
+ 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com'
connection = Connection.new(server, env)
connection.websocket.expects(:close)
diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb
index fb83bd7d77..d7e1041e68 100644
--- a/actioncable/test/connection/base_test.rb
+++ b/actioncable/test/connection/base_test.rb
@@ -1,5 +1,6 @@
require 'test_helper'
require 'stubs/test_server'
+require 'active_support/core_ext/object/json'
class ActionCable::Connection::BaseTest < ActionCable::TestCase
class Connection < ActionCable::Connection::Base
@@ -73,7 +74,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
connection.process
# Setup the connection
- Concurrent::TimerTask.stubs(:new).returns(true)
+ connection.server.stubs(:timer).returns(true)
connection.send :handle_open
assert connection.connected
@@ -119,7 +120,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
env = Rack::MockRequest.env_for(
"/test",
{ 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket',
- 'HTTP_ORIGIN' => 'http://rubyonrails.org', 'rack.hijack' => CallMeMaybe.new }
+ 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.org', 'rack.hijack' => CallMeMaybe.new }
)
connection = ActionCable::Connection::Base.new(@server, env)
@@ -131,7 +132,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
private
def open_connection
env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket',
- 'HTTP_ORIGIN' => 'http://rubyonrails.com'
+ 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com'
Connection.new(@server, env)
end
diff --git a/actioncable/test/connection/cross_site_forgery_test.rb b/actioncable/test/connection/cross_site_forgery_test.rb
index a29f65fb97..2d516b0533 100644
--- a/actioncable/test/connection/cross_site_forgery_test.rb
+++ b/actioncable/test/connection/cross_site_forgery_test.rb
@@ -76,6 +76,6 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase
def env_for_origin(origin)
Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', 'SERVER_NAME' => HOST,
- 'HTTP_ORIGIN' => origin
+ 'HTTP_HOST' => HOST, 'HTTP_ORIGIN' => origin
end
end
diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb
index 1019ad541e..c3d5f1f90b 100644
--- a/actioncable/test/connection/identifier_test.rb
+++ b/actioncable/test/connection/identifier_test.rb
@@ -64,7 +64,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
end
def open_connection(server:)
- env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
@connection = Connection.new(server, env)
@connection.process
diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb
index e9bb4e6d7f..484e73bb30 100644
--- a/actioncable/test/connection/multiple_identifiers_test.rb
+++ b/actioncable/test/connection/multiple_identifiers_test.rb
@@ -28,7 +28,7 @@ class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase
end
def open_connection(server:)
- env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
@connection = Connection.new(server, env)
@connection.process
diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb
index 9d0bda83ef..eca0c31060 100644
--- a/actioncable/test/connection/string_identifier_test.rb
+++ b/actioncable/test/connection/string_identifier_test.rb
@@ -30,7 +30,7 @@ class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase
end
def open_connection
- env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
@connection = Connection.new(@server, env)
@connection.process
diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb
index 68a79c6e4c..f91597f567 100644
--- a/actioncable/test/connection/subscriptions_test.rb
+++ b/actioncable/test/connection/subscriptions_test.rb
@@ -107,7 +107,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
end
def setup_connection
- env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
@connection = Connection.new(@server, env)
@subscriptions = ActionCable::Connection::Subscriptions.new(@connection)
diff --git a/actioncable/test/stubs/test_connection.rb b/actioncable/test/stubs/test_connection.rb
index da98201900..8ba284fdc6 100644
--- a/actioncable/test/stubs/test_connection.rb
+++ b/actioncable/test/stubs/test_connection.rb
@@ -1,18 +1,19 @@
require 'stubs/user'
class TestConnection
- attr_reader :identifiers, :logger, :current_user, :transmissions
+ attr_reader :identifiers, :logger, :current_user, :server, :transmissions
def initialize(user = User.new("lifo"))
@identifiers = [ :current_user ]
@current_user = user
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
+ @server = TestServer.new
@transmissions = []
end
def pubsub
- SuccessAdapter.new(TestServer.new)
+ SuccessAdapter.new(server)
end
def transmit(data)
diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb
index 5916cf1e83..9e860825f3 100644
--- a/actioncable/test/stubs/test_server.rb
+++ b/actioncable/test/stubs/test_server.rb
@@ -8,14 +8,24 @@ class TestServer
def initialize
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
@config = OpenStruct.new(log_tags: [], subscription_adapter: SuccessAdapter)
+ @config.use_faye = ENV['FAYE'].present?
+ @config.client_socket_class = if @config.use_faye
+ ActionCable::Connection::FayeClientSocket
+ else
+ ActionCable::Connection::ClientSocket
+ end
end
def pubsub
@config.subscription_adapter.new(self)
end
- def stream_event_loop
- @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new
+ def event_loop
+ @event_loop ||= if @config.use_faye
+ ActionCable::Connection::FayeEventLoop.new
+ else
+ ActionCable::Connection::StreamEventLoop.new
+ end
end
def worker_pool
diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb
index b31c2aa36c..82f0abbbf3 100644
--- a/actioncable/test/subscription_adapter/common.rb
+++ b/actioncable/test/subscription_adapter/common.rb
@@ -11,6 +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?
adapter_klass = server.config.pubsub_adapter
diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb
index 797e7786d1..030362d512 100644
--- a/actioncable/test/test_helper.rb
+++ b/actioncable/test/test_helper.rb
@@ -11,7 +11,41 @@ require 'active_support/core_ext/hash/indifferent_access'
# Require all the stubs and models
Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file }
-class ActionCable::TestCase < ActiveSupport::TestCase
+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
def wait_for_async
e = Concurrent.global_io_executor
until e.completed_task_count == e.scheduled_task_count
@@ -24,3 +58,11 @@ class ActionCable::TestCase < ActiveSupport::TestCase
wait_for_async
end
end
+
+class ActionCable::TestCase < ActiveSupport::TestCase
+ if ENV['FAYE'].present?
+ include EventMachineConcurrencyHelpers
+ else
+ include ConcurrentRubyConcurrencyHelpers
+ end
+end
diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
index 6d02b39225..419d6c7b93 100644
--- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
+++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
@@ -3,7 +3,7 @@ require 'base64'
module ActionMailer
# Implements a mailer preview interceptor that converts image tag src attributes
# that use inline cid: style urls to data: style urls so that they are visible
- # when previewing a HTML email in a web browser.
+ # when previewing an HTML email in a web browser.
#
# This interceptor is enabled by default. To disable it, delete it from the
# <tt>ActionMailer::Base.preview_interceptors</tt> array:
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index f8e0d9cf6c..e2535d024c 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -185,7 +185,7 @@ module ActionController
!request.fresh?(response)
end
- # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
+ # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
# instruction, so that intermediate caches must not cache the response.
#
# expires_in 20.minutes
@@ -195,7 +195,7 @@ module ActionController
# This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
#
- # The method will also ensure a HTTP Date header for client compatibility.
+ # The method will also ensure an HTTP Date header for client compatibility.
def expires_in(seconds, options = {})
response.cache_control.merge!(
:max_age => seconds,
@@ -208,7 +208,7 @@ module ActionController
response.date = Time.now unless response.date?
end
- # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
+ # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
# occur by the browser or intermediate caches (like caching proxy servers).
def expires_now
response.cache_control.replace(:no_cache => true)
@@ -216,7 +216,7 @@ module ActionController
# Cache or yield the block. The cache is supposed to never expire.
#
- # You can use this method when you have a HTTP response that never changes,
+ # You can use this method when you have an HTTP response that never changes,
# and the browser and proxies should cache it indefinitely.
#
# * +public+: By default, HTTP responses are private, cached only on the
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 700317614f..ecd21f29ce 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -428,7 +428,7 @@ module ActionController
end
alias xhr :xml_http_request
- # Simulate a HTTP request to +action+ by specifying request method,
+ # Simulate an HTTP request to +action+ by specifying request method,
# parameters and set/volley the response.
#
# - +action+: The controller action to call.
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index 8e899174c6..5c3b7245d6 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -115,7 +115,7 @@ module ActionDispatch
private
- # Converts a HTTP header name to an environment variable name if it is
+ # Converts an HTTP header name to an environment variable name if it is
# not contained within the headers hash.
def env_name(key)
key = key.to_s
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index cb442af19b..ab3077b308 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -35,9 +35,9 @@ module ActionDispatch
# expire HSTS immediately. Setting `hsts: false` is a shortcut for
# `hsts: { expires: 0 }`.
#
- # Redirection can be constrained to only whitelisted requests with `constrain_to`:
+ # Requests can opt-out of redirection with `exclude`:
#
- # config.ssl_options = { redirect: { constrain_to: -> request { request.path !~ /healthcheck/ } } }
+ # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
class SSL
# Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
# and greater than the 18-week requirement for browser preload lists.
@@ -59,7 +59,8 @@ module ActionDispatch
else
@redirect = redirect
end
- @constrain_to = @redirect && @redirect[:constrain_to] || proc { @redirect }
+
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
@secure_cookies = secure_cookies
if hsts != true && hsts != false && hsts[:subdomains].nil?
@@ -84,7 +85,7 @@ module ActionDispatch
flag_cookies_as_secure! headers if @secure_cookies
end
else
- return redirect_to_https request if @constrain_to.call(request)
+ return redirect_to_https request unless @exclude.call(request)
@app.call(env)
end
end
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index f807a8fe16..85f202b823 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -515,11 +515,17 @@ module ActionDispatch
named_routes[name] = route if name
if route.segment_keys.include?(:controller)
- ActiveSupport::Deprecation.warn("Using a dynamic :controller segment in a route is deprecated and will be remove in Rails 5.1")
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using a dynamic :controller segment in a route is deprecated and
+ will be removed in Rails 5.1
+ MSG
end
if route.segment_keys.include?(:action)
- ActiveSupport::Deprecation.warn("Using a dynamic :action segment in a route is deprecated and will be remove in Rails 5.1")
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using a dynamic :action segment in a route is deprecated and
+ will be removed in Rails 5.1
+ MSG
end
route
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index e7af27463c..44ad2c10d8 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -117,7 +117,7 @@ module ActionDispatch
# # Tests a route, providing a defaults hash
# assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
#
- # # Tests a route with a HTTP method
+ # # Tests a route with an HTTP method
# assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message)
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index bb2125e485..668b2b6cfe 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -39,11 +39,11 @@ class RedirectSSLTest < SSLTest
assert_equal redirect[:body].join, @response.body
end
- test 'constrain to can avoid redirect' do
- constraining = { constrain_to: -> request { request.path !~ /healthcheck/ } }
+ test 'exclude can avoid redirect' do
+ excluding = { exclude: -> request { request.path =~ /healthcheck/ } }
- assert_not_redirected 'http://example.org/healthcheck', redirect: constraining
- assert_redirected from: 'http://example.org/', redirect: constraining
+ assert_not_redirected 'http://example.org/healthcheck', redirect: excluding
+ assert_redirected from: 'http://example.org/', redirect: excluding
end
test 'https is not redirected' do
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 861f2c3c36..3bb4b5236e 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,11 @@
+* Fix an issue when preloading associations with extensions.
+ Previously every association with extension methods was transformed into an
+ instance dependent scope. This is no longer the case.
+
+ Fixes #23934.
+
+ *Yves Senn*
+
* Deprecate `{insert|update|delete}_sql` in `DatabaseStatements`.
Use the `{insert|update|delete}` public methods instead.
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 56a8dc4e18..f25bd7ca9f 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -70,7 +70,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.wrap_scope(scope, mod)
if scope
- proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ if scope.arity > 0
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { instance_exec(&scope).extending(mod) }
+ end
else
proc { extending(mod) }
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 3ee84fb66c..7f2a2229ee 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -1392,6 +1392,18 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal('10 was not recognized for preload', exception.message)
end
+ test "associations with extensions are not instance dependent" do
+ assert_nothing_raised do
+ Author.includes(:posts_with_extension).to_a
+ end
+ end
+
+ test "including associations with extensions and an instance dependent scope is not supported" do
+ e = assert_raises(ArgumentError) do
+ Author.includes(:posts_with_extension_and_instance).to_a
+ end
+ assert_match(/Preloading instance dependent scopes is not supported/, e.message)
+ end
test "preloading readonly association" do
# has-one
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index f25e31b13d..38b983eda0 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -144,6 +144,14 @@ class Author < ActiveRecord::Base
has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
+ has_many :posts_with_extension, -> { order(:title) }, class_name: "Post" do
+ def extension_method; end
+ end
+
+ has_many :posts_with_extension_and_instance, ->(record) { order(:title) }, class_name: "Post" do
+ def extension_method; end
+ end
+
attr_accessor :post_log
after_initialize :set_post_log
diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb
index e784556abf..2bd1c01d35 100644
--- a/activesupport/lib/active_support/execution_wrapper.rb
+++ b/activesupport/lib/active_support/execution_wrapper.rb
@@ -4,6 +4,10 @@ module ActiveSupport
class ExecutionWrapper
include ActiveSupport::Callbacks
+ Null = Object.new # :nodoc:
+ def Null.complete! # :nodoc:
+ end
+
define_callbacks :run
define_callbacks :complete
@@ -22,7 +26,11 @@ module ActiveSupport
#
# Where possible, prefer +wrap+.
def self.run!
- new.tap(&:run!)
+ if active?
+ Null
+ else
+ new.tap(&:run!)
+ end
end
# Perform the work in the supplied block as an execution.
@@ -43,17 +51,17 @@ module ActiveSupport
def self.inherited(other) # :nodoc:
super
- other.active = Concurrent::Hash.new(0)
+ other.active = Concurrent::Hash.new
end
- self.active = Concurrent::Hash.new(0)
+ self.active = Concurrent::Hash.new
def self.active? # :nodoc:
- @active[Thread.current] > 0
+ @active[Thread.current]
end
def run! # :nodoc:
- self.class.active[Thread.current] += 1
+ self.class.active[Thread.current] = true
run_callbacks(:run)
end
@@ -63,7 +71,8 @@ module ActiveSupport
# Where possible, prefer +wrap+.
def complete!
run_callbacks(:complete)
- self.class.active.delete Thread.current if (self.class.active[Thread.current] -= 1) == 0
+ ensure
+ self.class.active.delete Thread.current
end
end
end
diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb
index d88fcbcf78..5d1f0e1e66 100644
--- a/activesupport/lib/active_support/reloader.rb
+++ b/activesupport/lib/active_support/reloader.rb
@@ -22,8 +22,6 @@ module ActiveSupport
# unloaded.
#
class Reloader < ExecutionWrapper
- Null = Class.new(ExecutionWrapper) # :nodoc:
-
define_callbacks :prepare
define_callbacks :class_unload
@@ -54,7 +52,7 @@ module ActiveSupport
if check!
super
else
- Null.run!
+ Null
end
end
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index f26019c72e..ebd67a4adb 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -119,25 +119,16 @@ If you want to cache a fragment under certain conditions, you can use
The `render` helper can also cache individual templates rendered for a collection.
It can even one up the previous example with `each` by reading all cache
-templates at once instead of one by one. This is done automatically if the template
-rendered by the collection includes a `cache` call. Take a collection that renders
-a `products/_product.html.erb` partial for each element:
-
-```ruby
-render products
-```
-
-If `products/_product.html.erb` starts with a `cache` call like so:
+templates at once instead of one by one. This is done by passing `cached: true` when rendering the collection:
```html+erb
-<% cache product do %>
- <%= product.name %>
-<% end %>
+<%= render partial: 'products/product', collection: @products, cached: true %>
```
-All the cached templates from previous renders will be fetched at once with much
-greater speed. There's more info on how to make your templates [eligible for
-collection caching](http://api.rubyonrails.org/classes/ActionView/Template/Handlers/ERB.html#method-i-resource_cache_call_pattern).
+All cached templates from previous renders will be fetched at once with much
+greater speed. Additionally, the templates that haven't yet been cached will be
+written to cache and multi fetched on the next render.
+
### Russian Doll Caching
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 83173e8d75..614ca4dbe1 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -700,7 +700,7 @@ This would detect that there are no books with the specified ID, populate the `@
### Using `head` To Build Header-Only Responses
-The `head` method can be used to send responses with only headers to the browser. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing a HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header:
+The `head` method can be used to send responses with only headers to the browser. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing an HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header:
```ruby
head :bad_request
diff --git a/guides/source/security.md b/guides/source/security.md
index 98324141cc..f4a9f64669 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -381,7 +381,7 @@ Refer to the Injection section for countermeasures against XSS. It is _recommend
**CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
-A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/news/item/Symantec-reports-first-active-attack-on-a-DSL-router-735883.html). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for the user, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen.
+A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/news/item/Symantec-reports-first-active-attack-on-a-DSL-router-735883.html). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for the user, but it also contained an image tag that resulted in an HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen.
Another example changed Google Adsense's e-mail address and password. If the victim was logged into Google Adsense, the administration interface for Google advertisement campaigns, an attacker could change the credentials of the victim.

@@ -453,7 +453,7 @@ However, the attacker may also take over the account by changing the e-mail addr
#### Other
-Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in a HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
+Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in an HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
### CAPTCHAs
@@ -466,7 +466,7 @@ The problem with CAPTCHAs is that they have a negative impact on the user experi
Most bots are really dumb. They crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript.
-Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require a HTTPS request to Google ReCaptcha before computing the response.
+Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require an HTTPS request to Google ReCaptcha before computing the response.
Here are some ideas how to hide honeypot fields by JavaScript and/or CSS:
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 09eec7a64c..e302611b2a 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -798,7 +798,7 @@ and
can be set directly on the `@request` instance variable:
```ruby
-# setting a HTTP Header
+# setting an HTTP Header
@request.headers["Accept"] = "text/plain, text/html"
get articles_url # simulate the request with custom header