aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml5
-rw-r--r--actioncable/CHANGELOG.md20
-rw-r--r--actioncable/app/assets/javascripts/action_cable/connection.coffee41
-rw-r--r--actioncable/app/assets/javascripts/action_cable/consumer.coffee19
-rw-r--r--actioncable/app/assets/javascripts/action_cable/subscription.coffee6
-rw-r--r--actioncable/lib/action_cable.rb3
-rw-r--r--actioncable/lib/action_cable/connection/base.rb3
-rw-r--r--actioncable/lib/action_cable/connection/client_socket.rb8
-rw-r--r--actioncable/lib/action_cable/connection/faye_client_socket.rb9
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb8
-rw-r--r--actionmailer/CHANGELOG.md6
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb2
-rw-r--r--actionmailer/test/delivery_methods_test.rb2
-rw-r--r--actionpack/CHANGELOG.md2
-rw-r--r--actionpack/lib/action_controller/api.rb39
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb6
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb14
-rw-r--r--activerecord/test/models/owner.rb3
-rw-r--r--activerecord/test/models/pet.rb3
-rw-r--r--activerecord/test/models/pet_treasure.rb6
-rw-r--r--activerecord/test/schema/schema.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/marshal.rb2
-rw-r--r--guides/source/5_0_release_notes.md2
-rw-r--r--guides/source/caching_with_rails.md32
25 files changed, 193 insertions, 55 deletions
diff --git a/.travis.yml b/.travis.yml
index ef85107515..0747171567 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -58,3 +58,8 @@ services:
- rabbitmq
addons:
postgresql: "9.4"
+ apt:
+ sources:
+ - travis-ci/sqlite3
+ packages:
+ - sqlite3
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index d59e48e00c..5162a31cf8 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,3 +1,23 @@
+* WebSocket protocol negotiation.
+
+ Introduces an Action Cable protocol version that moves independently
+ of and, hopefully, more slowly than Action Cable itself. Client sockets
+ negotiate a protocol with the Cable server using WebSockets' native
+ subprotocol support:
+ * https://tools.ietf.org/html/rfc6455#section-1.9
+ * https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Subprotocols
+
+ If they can't negotiate a compatible protocol (usually due to upgrading
+ the Cable server with a browser still running old JavaScript) then the
+ client knows to disconnect, cease retrying, and tell the app that it hit
+ a protocol mismatch.
+
+ This allows us to evolve the Action Cable message format, handshaking,
+ pings, acknowledgements, and more without breaking older clients'
+ expectations of server behavior.
+
+ *Daniel Rhodes*
+
* Pubsub: automatic stream decoding.
stream_for @room, coder: ActiveSupport::JSON do |message|
diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee
index 3a139acf3a..822f25ac1d 100644
--- a/actioncable/app/assets/javascripts/action_cable/connection.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee
@@ -2,7 +2,8 @@
# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
-{message_types} = ActionCable.INTERNAL
+{message_types, protocols} = ActionCable.INTERNAL
+[supportedProtocols..., unsupportedProtocol] = protocols
class ActionCable.Connection
@reopenDelay: 500
@@ -10,6 +11,7 @@ class ActionCable.Connection
constructor: (@consumer) ->
{@subscriptions} = @consumer
@monitor = new ActionCable.ConnectionMonitor this
+ @disconnected = true
send: (data) ->
if @isOpen()
@@ -23,15 +25,16 @@ class ActionCable.Connection
ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}")
throw new Error("Existing connection must be closed before opening")
else
- ActionCable.log("Opening WebSocket, current state is #{@getState()}")
+ ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}")
@uninstallEventHandlers() if @webSocket?
- @webSocket = new WebSocket(@consumer.url)
+ @webSocket = new WebSocket(@consumer.url, protocols)
@installEventHandlers()
@monitor.start()
true
- close: ->
- @webSocket?.close()
+ close: ({allowReconnect} = {allowReconnect: true}) ->
+ @monitor.stop() unless allowReconnect
+ @webSocket?.close() if @isActive()
reopen: ->
ActionCable.log("Reopening WebSocket, current state is #{@getState()}")
@@ -46,6 +49,9 @@ class ActionCable.Connection
else
@open()
+ getProtocol: ->
+ @webSocket?.protocol
+
isOpen: ->
@isState("open")
@@ -54,6 +60,9 @@ class ActionCable.Connection
# Private
+ isProtocolSupported: ->
+ @getProtocol() in supportedProtocols
+
isState: (states...) ->
@getState() in states
@@ -74,10 +83,12 @@ class ActionCable.Connection
events:
message: (event) ->
+ return unless @isSupportedProtocol()
{identifier, message, type} = JSON.parse(event.data)
switch type
when message_types.welcome
@monitor.recordConnect()
+ @subscriptions.reload()
when message_types.ping
@monitor.recordPing()
when message_types.confirmation
@@ -88,20 +99,18 @@ class ActionCable.Connection
@subscriptions.notify(identifier, "received", message)
open: ->
- ActionCable.log("WebSocket onopen event")
+ ActionCable.log("WebSocket onopen event, using '#{@getProtocol()}' subprotocol")
@disconnected = false
- @subscriptions.reload()
+ if not @isProtocolSupported()
+ ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.")
+ @close(allowReconnect: false)
- close: ->
+ close: (event) ->
ActionCable.log("WebSocket onclose event")
- @disconnect()
+ return if @disconnected
+ @disconnected = true
+ @monitor.recordDisconnect()
+ @subscriptions.notifyAll("disconnected", {willAttemptReconnect: @monitor.isRunning()})
error: ->
ActionCable.log("WebSocket onerror event")
- @disconnect()
-
- disconnect: ->
- return if @disconnected
- @disconnected = true
- @subscriptions.notifyAll("disconnected")
- @monitor.recordDisconnect()
diff --git a/actioncable/app/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee
index 7aae1ed8ed..3298be717f 100644
--- a/actioncable/app/assets/javascripts/action_cable/consumer.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/consumer.coffee
@@ -14,6 +14,19 @@
# App.appearance = App.cable.subscriptions.create "AppearanceChannel"
#
# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
+#
+# When a consumer is created, it automatically connects with the server.
+#
+# To disconnect from the server, call
+#
+# App.cable.disconnect()
+#
+# and to restart the connection:
+#
+# App.cable.connect()
+#
+# Any channel subscriptions which existed prior to disconnecting will
+# automatically resubscribe.
class ActionCable.Consumer
constructor: (@url) ->
@subscriptions = new ActionCable.Subscriptions this
@@ -22,6 +35,12 @@ class ActionCable.Consumer
send: (data) ->
@connection.send(data)
+ connect: ->
+ @connection.open()
+
+ disconnect: ->
+ @connection.close(allowReconnect: false)
+
ensureActiveConnection: ->
unless @connection.isActive()
@connection.open()
diff --git a/actioncable/app/assets/javascripts/action_cable/subscription.coffee b/actioncable/app/assets/javascripts/action_cable/subscription.coffee
index 61a3fb1309..8e0805a174 100644
--- a/actioncable/app/assets/javascripts/action_cable/subscription.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/subscription.coffee
@@ -8,6 +8,12 @@
# connected: ->
# # Called once the subscription has been successfully completed
#
+# disconnected: ({ willAttemptReconnect: boolean }) ->
+# # Called when the client has disconnected with the server.
+# # The object will have an `willAttemptReconnect` property which
+# # says whether the client has the intention of attempting
+# # to reconnect.
+#
# appear: ->
# @perform 'appear', appearing_on: @appearingOn()
#
diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb
index 68a5fff3e7..b6d2842867 100644
--- a/actioncable/lib/action_cable.rb
+++ b/actioncable/lib/action_cable.rb
@@ -35,7 +35,8 @@ module ActionCable
confirmation: 'confirm_subscription'.freeze,
rejection: 'reject_subscription'.freeze
},
- default_mount_path: '/cable'.freeze
+ default_mount_path: '/cable'.freeze,
+ protocols: ["actioncable-v1-json".freeze, "actioncable-unsupported".freeze].freeze
}
# Singleton instance of the server
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index 604a889bb0..9a7dfbe761 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -48,7 +48,7 @@ module ActionCable
include InternalChannel
include Authorization
- attr_reader :server, :env, :subscriptions, :logger, :worker_pool
+ attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
delegate :event_loop, :pubsub, to: :server
def initialize(server, env, coder: ActiveSupport::JSON)
@@ -163,6 +163,7 @@ module ActionCable
end
def handle_open
+ @protocol = websocket.protocol
connect if respond_to?(:connect)
subscribe_to_internal_channel
send_welcome_message
diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb
index 7d6de78582..6f29f32ea9 100644
--- a/actioncable/lib/action_cable/connection/client_socket.rb
+++ b/actioncable/lib/action_cable/connection/client_socket.rb
@@ -29,7 +29,7 @@ module ActionCable
attr_reader :env, :url
- def initialize(env, event_target, event_loop)
+ def initialize(env, event_target, event_loop, protocols)
@env = env
@event_target = event_target
@event_loop = event_loop
@@ -42,7 +42,7 @@ module ActionCable
@ready_state = CONNECTING
# The driver calls +env+, +url+, and +write+
- @driver = ::WebSocket::Driver.rack(self)
+ @driver = ::WebSocket::Driver.rack(self, protocols: protocols)
@driver.on(:open) { |e| open }
@driver.on(:message) { |e| receive_message(e.data) }
@@ -111,6 +111,10 @@ module ActionCable
@ready_state == OPEN
end
+ def protocol
+ @driver.protocol
+ end
+
private
def open
return unless @ready_state == CONNECTING
diff --git a/actioncable/lib/action_cable/connection/faye_client_socket.rb b/actioncable/lib/action_cable/connection/faye_client_socket.rb
index 47d09a9e14..a4bfe7db17 100644
--- a/actioncable/lib/action_cable/connection/faye_client_socket.rb
+++ b/actioncable/lib/action_cable/connection/faye_client_socket.rb
@@ -3,9 +3,10 @@ require 'faye/websocket'
module ActionCable
module Connection
class FayeClientSocket
- def initialize(env, event_target, stream_event_loop)
+ def initialize(env, event_target, stream_event_loop, protocols)
@env = env
@event_target = event_target
+ @protocols = protocols
@faye = nil
end
@@ -23,6 +24,10 @@ module ActionCable
@faye && @faye.close
end
+ def protocol
+ @faye && @faye.protocol
+ end
+
def rack_response
connect
@faye.rack_response
@@ -31,7 +36,7 @@ module ActionCable
private
def connect
return if @faye
- @faye = Faye::WebSocket.new(@env)
+ @faye = Faye::WebSocket.new(@env, @protocols)
@faye.on(:open) { |event| @event_target.on_open }
@faye.on(:message) { |event| @event_target.on_message(event.data) }
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 0bec9b6a96..11f28c37e8 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)
- @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop) : nil
+ 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
end
def possible?
@@ -24,6 +24,10 @@ module ActionCable
websocket.close
end
+ def protocol
+ websocket.protocol
+ end
+
def rack_response
websocket.rack_response
end
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 51d85b95f2..1109912b04 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,3 +1,9 @@
+* Removes `-t` from default Sendmail arguments to match the underlying
+ `Mail::Sendmail` setting.
+
+ *Clayton Liggitt*
+
+
## Rails 5.0.0.beta3 (February 24, 2016) ##
* Add support for fragment caching in Action Mailer views.
diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb
index 4758b55a2a..571c8e7d2a 100644
--- a/actionmailer/lib/action_mailer/delivery_methods.rb
+++ b/actionmailer/lib/action_mailer/delivery_methods.rb
@@ -36,7 +36,7 @@ module ActionMailer
add_delivery_method :sendmail, Mail::Sendmail,
location: '/usr/sbin/sendmail',
- arguments: '-i -t'
+ arguments: '-i'
add_delivery_method :test, Mail::TestMailer
end
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 2786fe0d07..bcbd036f26 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -39,7 +39,7 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
test "default sendmail settings" do
settings = {
location: '/usr/sbin/sendmail',
- arguments: '-i -t'
+ arguments: '-i'
}
assert_equal settings, ActionMailer::Base.sendmail_settings
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 20321eece4..e0ac6c24b1 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -211,7 +211,7 @@
*Derek Prior*
-* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1
+* `ActionController::TestCase` will be moved to its own gem in Rails 5.1
With the speed improvements made to `ActionDispatch::IntegrationTest` we no
longer need to keep two separate code bases for testing controllers. In
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb
index ff12705abe..6bbebb7b4c 100644
--- a/actionpack/lib/action_controller/api.rb
+++ b/actionpack/lib/action_controller/api.rb
@@ -14,22 +14,22 @@ module ActionController
# flash, assets, and so on. This makes the entire controller stack thinner,
# suitable for API applications. It doesn't mean you won't have such
# features if you need them: they're all available for you to include in
- # your application, they're just not part of the default API Controller stack.
+ # your application, they're just not part of the default API controller stack.
#
- # By default, only the ApplicationController in a \Rails application inherits
- # from <tt>ActionController::API</tt>. All other controllers in turn inherit
- # from ApplicationController.
+ # Normally, +ApplicationController+ is the only controller that inherits from
+ # <tt>ActionController::API</tt>. All other controllers in turn inherit from
+ # +ApplicationController+.
#
# A sample controller could look like this:
#
# class PostsController < ApplicationController
# def index
- # @posts = Post.all
- # render json: @posts
+ # posts = Post.all
+ # render json: posts
# end
# end
#
- # Request, response and parameters objects all work the exact same way as
+ # Request, response, and parameters objects all work the exact same way as
# <tt>ActionController::Base</tt>.
#
# == Renders
@@ -37,18 +37,18 @@ module ActionController
# The default API Controller stack includes all renderers, which means you
# can use <tt>render :json</tt> and brothers freely in your controllers. Keep
# in mind that templates are not going to be rendered, so you need to ensure
- # your controller is calling either <tt>render</tt> or <tt>redirect</tt> in
- # all actions, otherwise it will return 204 No Content response.
+ # your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
+ # all actions, otherwise it will return 204 No Content.
#
# def show
- # @post = Post.find(params[:id])
- # render json: @post
+ # post = Post.find(params[:id])
+ # render json: post
# end
#
# == Redirects
#
# Redirects are used to move from one action to another. You can use the
- # <tt>redirect</tt> method in your controllers in the same way as
+ # <tt>redirect_to</tt> method in your controllers in the same way as in
# <tt>ActionController::Base</tt>. For example:
#
# def create
@@ -56,7 +56,7 @@ module ActionController
# # do stuff here
# end
#
- # == Adding new behavior
+ # == Adding New Behavior
#
# In some scenarios you may want to add back some functionality provided by
# <tt>ActionController::Base</tt> that is not present by default in
@@ -72,18 +72,19 @@ module ActionController
#
# class PostsController < ApplicationController
# def index
- # @posts = Post.all
+ # posts = Post.all
#
# respond_to do |format|
- # format.json { render json: @posts }
- # format.xml { render xml: @posts }
+ # format.json { render json: posts }
+ # format.xml { render xml: posts }
# end
# end
# end
#
- # Quite straightforward. Make sure to check <tt>ActionController::Base</tt>
- # available modules if you want to include any other functionality that is
- # not provided by <tt>ActionController::API</tt> out of the box.
+ # Quite straightforward. Make sure to check the modules included in
+ # <tt>ActionController::Base</tt> if you want to use any other
+ # functionality that is not provided by <tt>ActionController::API</tt>
+ # out of the box.
class API < Metal
abstract!
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index ecf6fb8643..e64af84e1a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -184,6 +184,7 @@ module ActiveRecord
def self.new(klass, owners, reflection, preload_scope); self; end
def self.run(preloader); end
def self.preloaded_records; []; end
+ def self.owners; []; end
end
# Returns a class containing the logic needed to load preload the data
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index f6766b996f..fe58fce313 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -288,9 +288,9 @@ module ActiveRecord
# create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
else
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
end
end
@@ -299,7 +299,7 @@ module ActiveRecord
# Example:
# drop_database('sebastian_development')
def drop_database(name) #:nodoc:
- execute "DROP DATABASE IF EXISTS `#{name}`"
+ execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
def current_database
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index bb8c9fa19c..aff0dabee7 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -11,7 +11,9 @@ require 'models/tagging'
require 'models/author'
require 'models/owner'
require 'models/pet'
+require 'models/pet_treasure'
require 'models/toy'
+require 'models/treasure'
require 'models/contract'
require 'models/company'
require 'models/developer'
@@ -1082,6 +1084,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [], person.posts
end
+ def test_preloading_empty_through_with_polymorphic_source_association
+ owner = Owner.create!(name: "Rainbow Unicat")
+ pet = Pet.create!(owner: owner)
+ person = Person.create!(first_name: "Gaga")
+ treasure = Treasure.create!(looter: person)
+ non_looted_treasure = Treasure.create!()
+ PetTreasure.create!(pet: pet, treasure: treasure, rainbow_color: "Ultra violet indigo")
+ PetTreasure.create!(pet: pet, treasure: non_looted_treasure, rainbow_color: "Ultra violet indigo")
+
+ assert_equal [person], Owner.where(name: "Rainbow Unicat").includes(pets: :persons).first.persons.to_a
+ end
+
def test_explicitly_joining_join_table
assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet
end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index cedb774b10..ce8242cf2f 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,7 +1,8 @@
class Owner < ActiveRecord::Base
self.primary_key = :owner_id
has_many :pets, -> { order 'pets.name desc' }
- has_many :toys, :through => :pets
+ has_many :toys, through: :pets
+ has_many :persons, through: :pets
belongs_to :last_pet, class_name: 'Pet'
scope :including_last_pet, -> {
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index f7970d7aab..53489fa1b3 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -4,6 +4,9 @@ class Pet < ActiveRecord::Base
self.primary_key = :pet_id
belongs_to :owner, :touch => true
has_many :toys
+ has_many :pet_treasures
+ has_many :treasures, through: :pet_treasures
+ has_many :persons, through: :treasures, source: :looter, source_type: 'Person'
class << self
attr_accessor :after_destroy_output
diff --git a/activerecord/test/models/pet_treasure.rb b/activerecord/test/models/pet_treasure.rb
new file mode 100644
index 0000000000..1fe7807ffe
--- /dev/null
+++ b/activerecord/test/models/pet_treasure.rb
@@ -0,0 +1,6 @@
+class PetTreasure < ActiveRecord::Base
+ self.table_name = "pets_treasures"
+
+ belongs_to :pet
+ belongs_to :treasure
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 2bcdc8729e..1027bcb365 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -614,6 +614,12 @@ ActiveRecord::Schema.define do
end
end
+ create_table :pets_treasures, force: true do |t|
+ t.column :treasure_id, :integer
+ t.column :pet_id, :integer
+ t.column :rainbow_color, :string
+ end
+
create_table :pirates, force: true do |t|
t.column :catchphrase, :string
t.column :parrot_id, :integer
diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb
index 5875ae5f71..edfc8296fe 100644
--- a/activesupport/lib/active_support/core_ext/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -3,7 +3,7 @@ module ActiveSupport
def load(source)
super(source)
rescue ArgumentError, NameError => exc
- if exc.message.match(%r|undefined class/module (.+?)(::)?\z|)
+ if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|)
# try loading the class/module
loaded = $1.constantize
diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md
index 45e9b77eea..3b926c04f0 100644
--- a/guides/source/5_0_release_notes.md
+++ b/guides/source/5_0_release_notes.md
@@ -270,7 +270,7 @@ Please refer to the [Changelog][action-pack] for detailed changes.
* Changed the `protect_from_forgery` prepend default to `false`.
([commit](https://github.com/rails/rails/commit/39794037817703575c35a75f1961b01b83791191))
-* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1. Use
+* `ActionController::TestCase` will be moved to its own gem in Rails 5.1. Use
`ActionDispatch::IntegrationTest` instead.
([commit](https://github.com/rails/rails/commit/4414c5d1795e815b102571425974a8b1d46d932d))
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index ec4444eb43..ae204a55d6 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -512,12 +512,38 @@ class ProductsController < ApplicationController
end
```
-### A note on weak ETags
+### Strong v/s Weak ETags
-Etags generated by Rails are weak by default. Weak etags allow semantically equivalent responses to have the same etags, even if their bodies do not match exactly. This is useful when we don't want the page to be regenerated for minor changes in response body. If you absolutely need to generate a strong etag, it can be assigned to the header directly.
+Rails generates weak ETags by default. Weak ETags allow semantically equivalent
+responses to have the same ETags, even if their bodies do not match exactly.
+This is useful when we don't want the page to be regenerated for minor changes in
+response body.
+
+Weak ETags have a leading `W/` to differentiate them from strong ETags.
+
+```
+ W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag
+ "618bbc92e2d35ea1945008b42799b0e7" → Strong ETag
+```
+
+Unlike weak ETag, Strong ETag implies that response should be exactly same
+and byte by byte identical. Useful when doing Range requests within a
+large video or PDF file. Some CDNs support only strong ETags, like Akamai.
+If you absolutely need to generate a strong ETag, it can be done as follows.
+
+```ruby
+ class ProductsController < ApplicationController
+ def show
+ @product = Product.find(params[:id])
+ fresh_when last_modified: @product.published_at.utc, strong_etag: @product
+ end
+ end
+```
+
+You can also set the strong ETag directly on the response.
```ruby
- response.add_header "ETag", Digest::MD5.hexdigest(response.body)
+ response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"
```
References