diff options
42 files changed, 360 insertions, 192 deletions
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index 1ea862b389..d59e48e00c 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,11 +1,16 @@ -* Add ActiveSupport::Notifications to ActionCable::Channel. +* Pubsub: automatic stream decoding. - *Matthew Wear* + stream_for @room, coder: ActiveSupport::JSON do |message| + # `message` is a Ruby hash here instead of a JSON string -* Allow channel identifiers with no backslahes/escaping to be accepted - by the subscription storer. + The `coder` must respond to `#decode`. Defaults to `coder: nil` + which skips decoding entirely. - *Jon Moss* + *Jeremy Daer* + +* Add ActiveSupport::Notifications to ActionCable::Channel. + + *Matthew Wear* * Safely support autoloading and class unloading, by preventing concurrent loads, and disconnecting all cables during reload. diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb index 464d0581dd..845b747fc5 100644 --- a/actioncable/lib/action_cable/channel/base.rb +++ b/actioncable/lib/action_cable/channel/base.rb @@ -198,7 +198,7 @@ module ActionCable payload = { channel_class: self.class.name, data: data, via: via } ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do - connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data) + connection.transmit identifier: @identifier, message: data end end @@ -274,7 +274,7 @@ module ActionCable logger.info "#{self.class.name} is transmitting the subscription confirmation" ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do - connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]) + connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation] @subscription_confirmation_sent = true end end @@ -289,7 +289,7 @@ module ActionCable logger.info "#{self.class.name} is transmitting the subscription rejection" ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do - connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]) + connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection] end end end diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb index 23d7320a28..f654ce0bfa 100644 --- a/actioncable/lib/action_cable/channel/streams.rb +++ b/actioncable/lib/action_cable/channel/streams.rb @@ -46,9 +46,7 @@ module ActionCable # def subscribed # @room = Chat::Room[params[:room_number]] # - # stream_for @room, -> (encoded_message) do - # message = ActiveSupport::JSON.decode(encoded_message) - # + # stream_for @room, coder: ActiveSupport::JSON do |message| # if message['originated_at'].present? # elapsed_time = (Time.now.to_f - message['originated_at']).round(2) # @@ -71,16 +69,23 @@ module ActionCable # Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used # instead of the default of just transmitting the updates straight to the subscriber. - def stream_from(broadcasting, callback = nil) + # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback. + # Defaults to `coder: nil` which does no decoding, passes raw messages. + def stream_from(broadcasting, callback = nil, coder: nil, &block) broadcasting = String(broadcasting) # Don't send the confirmation until pubsub#subscribe is successful defer_subscription_confirmation! - callback ||= default_stream_callback(broadcasting) - streams << [ broadcasting, callback ] + if handler = callback || block + handler = -> message { handler.(coder.decode(message)) } if coder + else + handler = default_stream_handler(broadcasting, coder: coder) + end + + streams << [ broadcasting, handler ] connection.server.event_loop.post do - pubsub.subscribe(broadcasting, callback, lambda do + pubsub.subscribe(broadcasting, handler, lambda do transmit_subscription_confirmation logger.info "#{self.class.name} is streaming from #{broadcasting}" end) @@ -90,8 +95,11 @@ module ActionCable # Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a # <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight # to the subscriber. - def stream_for(model, callback = nil) - stream_from(broadcasting_for([ channel_name, model ]), callback) + # + # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback. + # Defaults to `coder: nil` which does no decoding, passes raw messages. + def stream_for(model, callback = nil, coder: nil, &block) + stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder) end # Unsubscribes all streams associated with this channel from the pubsub queue. @@ -109,9 +117,11 @@ module ActionCable @_streams ||= [] end - def default_stream_callback(broadcasting) + def default_stream_handler(broadcasting, coder:) + coder ||= ActiveSupport::JSON + -> (message) do - transmit ActiveSupport::JSON.decode(message), via: "streamed from #{broadcasting}" + transmit coder.decode(message), via: "streamed from #{broadcasting}" end end end diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index b4488265cb..604a889bb0 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -51,8 +51,8 @@ module ActionCable attr_reader :server, :env, :subscriptions, :logger, :worker_pool delegate :event_loop, :pubsub, to: :server - def initialize(server, env) - @server, @env = server, env + def initialize(server, env, coder: ActiveSupport::JSON) + @server, @env, @coder = server, env, coder @worker_pool = server.worker_pool @logger = new_tagged_logger @@ -67,7 +67,7 @@ module ActionCable # Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user. # This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks. - def process # :nodoc: + def process #:nodoc: logger.info started_request_message if websocket.possible? && allow_request_origin? @@ -77,20 +77,22 @@ module ActionCable end end - # Data received over the WebSocket connection is handled by this method. It's expected that everything inbound is JSON encoded. - # The data is routed to the proper channel that the connection has subscribed to. - def receive(data_in_json) + # Decodes WebSocket messages and dispatches them to subscribed channels. + # WebSocket message transfer encoding is always JSON. + def receive(websocket_message) #:nodoc: + send_async :dispatch_websocket_message, websocket_message + end + + def dispatch_websocket_message(websocket_message) #:nodoc: if websocket.alive? - subscriptions.execute_command ActiveSupport::JSON.decode(data_in_json) + subscriptions.execute_command decode(websocket_message) else - logger.error "Received data without a live WebSocket (#{data_in_json.inspect})" + logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})" end end - # Send raw data straight back down the WebSocket. This is not intended to be called directly. Use the #transmit available on the - # Channel instead, as that'll automatically address the correct subscriber and wrap the message in JSON. - def transmit(data) # :nodoc: - websocket.transmit data + def transmit(cable_message) # :nodoc: + websocket.transmit encode(cable_message) end # Close the WebSocket connection. @@ -115,7 +117,7 @@ module ActionCable end def beat - transmit ActiveSupport::JSON.encode(type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i) + transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i end def on_open # :nodoc: @@ -152,6 +154,14 @@ module ActionCable attr_reader :message_buffer private + def encode(cable_message) + @coder.encode cable_message + end + + def decode(websocket_message) + @coder.decode websocket_message + end + def handle_open connect if respond_to?(:connect) subscribe_to_internal_channel @@ -178,7 +188,7 @@ module ActionCable # Send welcome message to the internal connection monitor channel. # This ensures the connection monitor state is reset after a successful # websocket connection. - transmit ActiveSupport::JSON.encode(type: ActionCable::INTERNAL[:message_types][:welcome]) + transmit type: ActionCable::INTERNAL[:message_types][:welcome] end def allow_request_origin? diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb index 3c5d39f59a..f70d52f99b 100644 --- a/actioncable/lib/action_cable/connection/internal_channel.rb +++ b/actioncable/lib/action_cable/connection/internal_channel.rb @@ -11,7 +11,7 @@ module ActionCable def subscribe_to_internal_channel if connection_identifier.present? - callback = -> (message) { process_internal_message(message) } + callback = -> (message) { process_internal_message decode(message) } @_internal_subscriptions ||= [] @_internal_subscriptions << [ internal_channel, callback ] @@ -27,8 +27,6 @@ module ActionCable end def process_internal_message(message) - message = ActiveSupport::JSON.decode(message) - case message['type'] when 'disconnect' logger.info "Removing connection (#{connection_identifier})" diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb index 19f2e6e918..6a80770cae 100644 --- a/actioncable/lib/action_cable/connection/message_buffer.rb +++ b/actioncable/lib/action_cable/connection/message_buffer.rb @@ -30,7 +30,7 @@ module ActionCable protected attr_reader :connection - attr_accessor :buffered_messages + attr_reader :buffered_messages private def valid?(message) @@ -38,7 +38,7 @@ module ActionCable end def receive(message) - connection.send_async :receive, message + connection.receive message end def buffer(message) diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb index 5aa907c2d3..3742f248d1 100644 --- a/actioncable/lib/action_cable/connection/subscriptions.rb +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -23,13 +23,13 @@ module ActionCable end def add(data) - id_options = decode_hash(data['identifier']) - identifier = normalize_identifier(id_options) + id_key = data['identifier'] + id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access subscription_klass = connection.server.channel_classes[id_options[:channel]] if subscription_klass - subscriptions[identifier] ||= subscription_klass.new(connection, identifier, id_options) + subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options) else logger.error "Subscription class not found (#{data.inspect})" end @@ -37,7 +37,7 @@ module ActionCable def remove(data) logger.info "Unsubscribing from channel: #{data['identifier']}" - remove_subscription subscriptions[normalize_identifier(data['identifier'])] + remove_subscription subscriptions[data['identifier']] end def remove_subscription(subscription) @@ -46,7 +46,7 @@ module ActionCable end def perform_action(data) - find(data).perform_action(decode_hash(data['data'])) + find(data).perform_action ActiveSupport::JSON.decode(data['data']) end def identifiers @@ -63,21 +63,8 @@ module ActionCable private delegate :logger, to: :connection - def normalize_identifier(identifier) - identifier = ActiveSupport::JSON.encode(identifier) if identifier.is_a?(Hash) - identifier - end - - # If `data` is a Hash, this means that the original JSON - # sent by the client had no backslashes in it, and does - # not need to be decoded again. - def decode_hash(data) - data = ActiveSupport::JSON.decode(data) unless data.is_a?(Hash) - data.with_indifferent_access - end - def find(data) - if subscription = subscriptions[normalize_identifier(data['identifier'])] + if subscription = subscriptions[data['identifier']] subscription else raise "Unable to find subscription with identifier: #{data['identifier']}" diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb index 778f5ffeed..b1a0e11631 100644 --- a/actioncable/lib/action_cable/server/base.rb +++ b/actioncable/lib/action_cable/server/base.rb @@ -52,7 +52,17 @@ module ActionCable @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. + # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread. + # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out + # at 4 worker threads by default. Tune the size yourself with config.action_cable.worker_pool_size. + # + # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool. + # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database + # connections. + # + # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe + # the db connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger + # db connection pool instead. def worker_pool @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) } end diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb index 98025f27f2..8f93564113 100644 --- a/actioncable/lib/action_cable/server/broadcasting.rb +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -19,27 +19,28 @@ module ActionCable # new Notification data['title'], body: data['body'] module Broadcasting # Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded. - def broadcast(broadcasting, message) - broadcaster_for(broadcasting).broadcast(message) + def broadcast(broadcasting, message, coder: ActiveSupport::JSON) + broadcaster_for(broadcasting, coder: coder).broadcast(message) end # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that # may need multiple spots to transmit to a specific broadcasting over and over. - def broadcaster_for(broadcasting) - Broadcaster.new(self, String(broadcasting)) + def broadcaster_for(broadcasting, coder: ActiveSupport::JSON) + Broadcaster.new(self, String(broadcasting), coder: coder) end private class Broadcaster - attr_reader :server, :broadcasting + attr_reader :server, :broadcasting, :coder - def initialize(server, broadcasting) - @server, @broadcasting = server, broadcasting + def initialize(server, broadcasting, coder:) + @server, @broadcasting, @coder = server, broadcasting, coder end def broadcast(message) - server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}" - server.pubsub.broadcast broadcasting, ActiveSupport::JSON.encode(message) + server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" + encoded = coder ? coder.encode(message) : message + server.pubsub.broadcast broadcasting, encoded end end end diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb index 5fe71caed2..0bb378cf03 100644 --- a/actioncable/lib/action_cable/server/configuration.rb +++ b/actioncable/lib/action_cable/server/configuration.rb @@ -14,7 +14,7 @@ module ActionCable @log_tags = [] @connection_class = ActionCable::Connection::Base - @worker_pool_size = 100 + @worker_pool_size = 4 @disable_request_forgery_protection = false end diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb index bed54eb6b3..daa782eeb3 100644 --- a/actioncable/test/channel/base_test.rb +++ b/actioncable/test/channel/base_test.rb @@ -146,12 +146,12 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase test "transmitting data" do @channel.perform_action 'action' => :get_latest - expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "message" => { "data" => "latest" } + expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" }} assert_equal expected, @connection.last_transmission end test "subscription confirmation" do - expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "confirm_subscription" + expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } assert_equal expected, @connection.last_transmission end diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb index aa93396d44..15db57d6ba 100644 --- a/actioncable/test/channel/rejection_test.rb +++ b/actioncable/test/channel/rejection_test.rb @@ -18,7 +18,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase @connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) } @channel = SecretChannel.new @connection, "{id: 1}", { id: 1 } - expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "reject_subscription" + expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" } assert_equal expected, @connection.last_transmission end diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb index 526ea92e4f..f51f19eb7d 100644 --- a/actioncable/test/channel/stream_test.rb +++ b/actioncable/test/channel/stream_test.rb @@ -2,18 +2,43 @@ require 'test_helper' require 'stubs/test_connection' require 'stubs/room' -class ActionCable::Channel::StreamTest < ActionCable::TestCase +module ActionCable::StreamTests + class Connection < ActionCable::Connection::Base + attr_reader :websocket + + def send_async(method, *args) + send method, *args + end + end + class ChatChannel < ActionCable::Channel::Base def subscribed if params[:id] @room = Room.new params[:id] - stream_from "test_room_#{@room.id}" + stream_from "test_room_#{@room.id}", coder: pick_coder(params[:coder]) end end def send_confirmation transmit_subscription_confirmation end + + private def pick_coder(coder) + case coder + when nil, 'json' + ActiveSupport::JSON + when 'custom' + DummyEncoder + when 'none' + nil + end + end + end + + module DummyEncoder + extend self + def encode(*) '{ "foo": "encoded" }' end + def decode(*) { foo: 'decoded' } end end class SymbolChannel < ActionCable::Channel::Base @@ -22,69 +47,114 @@ class ActionCable::Channel::StreamTest < ActionCable::TestCase end end - test "streaming start and stop" do - run_in_eventmachine do - 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 } + class StreamTest < ActionCable::TestCase + test "streaming start and stop" do + run_in_eventmachine do + 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 } - connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } - channel.unsubscribe_from_channel + connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } + channel.unsubscribe_from_channel + end end - end - test "stream from non-string channel" do - run_in_eventmachine do - 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, "" + test "stream from non-string channel" do + run_in_eventmachine do + 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, "" - connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } - channel.unsubscribe_from_channel + connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } + channel.unsubscribe_from_channel + end end - end - test "stream_for" do - run_in_eventmachine do - connection = TestConnection.new - connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:channel:stream_test:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) } + test "stream_for" do + run_in_eventmachine do + connection = TestConnection.new + 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.stream_for Room.new(1) + channel = ChatChannel.new connection, "" + channel.stream_for Room.new(1) + end end - end - test "stream_from subscription confirmation" do - run_in_eventmachine do - connection = TestConnection.new + test "stream_from subscription confirmation" do + run_in_eventmachine do + connection = TestConnection.new - ChatChannel.new connection, "{id: 1}", { id: 1 } - assert_nil connection.last_transmission + ChatChannel.new connection, "{id: 1}", { id: 1 } + assert_nil connection.last_transmission - wait_for_async + wait_for_async - expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "confirm_subscription" - connection.transmit(expected) + confirmation = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } + connection.transmit(confirmation) - assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation within 0.1s" + assert_equal confirmation, connection.last_transmission, "Did not receive subscription confirmation within 0.1s" + end end - end - test "subscription confirmation should only be sent out once" do - run_in_eventmachine do - connection = TestConnection.new + test "subscription confirmation should only be sent out once" do + run_in_eventmachine do + connection = TestConnection.new - channel = ChatChannel.new connection, "test_channel" - channel.send_confirmation - channel.send_confirmation + channel = ChatChannel.new connection, "test_channel" + channel.send_confirmation + channel.send_confirmation - wait_for_async + wait_for_async - expected = ActiveSupport::JSON.encode "identifier" => "test_channel", "type" => "confirm_subscription" - assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation" + expected = { "identifier" => "test_channel", "type" => "confirm_subscription" } + assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation" - assert_equal 1, connection.transmissions.size + assert_equal 1, connection.transmissions.size + end end end + require 'action_cable/subscription_adapter/inline' + + class StreamEncodingTest < ActionCable::TestCase + setup do + @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Inline) + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + @server.stubs(:channel_classes).returns(ChatChannel.name => ChatChannel) + end + + test 'custom encoder' do + run_in_eventmachine do + connection = open_connection + subscribe_to connection, identifiers: { id: 1 } + + connection.websocket.expects(:transmit) + @server.broadcast 'test_room_1', { foo: 'bar' }, coder: DummyEncoder + wait_for_async + end + end + + private + def subscribe_to(connection, identifiers:) + receive connection, command: 'subscribe', identifiers: identifiers + end + + def open_connection + env = Rack::MockRequest.env_for '/test', 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', 'HTTP_ORIGIN' => 'http://rubyonrails.com' + + Connection.new(@server, env).tap do |connection| + connection.process + assert connection.websocket.possible? + + wait_for_async + assert connection.websocket.alive? + end + end + + def receive(connection, command:, identifiers:) + identifier = JSON.generate(channel: 'ActionCable::StreamTests::ChatChannel', **identifiers) + connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier) + wait_for_async + end + end end diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb index 5f5c09d1a1..5ac453db35 100644 --- a/actioncable/test/client_test.rb +++ b/actioncable/test/client_test.rb @@ -75,11 +75,11 @@ class ClientTest < ActionCable::TestCase end @ws.on(:message) do |event| - hash = JSON.parse(event.data) - if hash['type'] == 'ping' + message = JSON.parse(event.data) + if message['type'] == 'ping' @pings += 1 else - @messages << hash + @messages << message @has_messages.release end end @@ -116,8 +116,8 @@ class ClientTest < ActionCable::TestCase list end - def send_message(hash) - @ws.send(JSON.dump(hash)) + def send_message(message) + @ws.send(JSON.generate(message)) end def close @@ -148,9 +148,9 @@ class ClientTest < ActionCable::TestCase with_puma_server do |port| c = faye_client(port) assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack - c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel') + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) - c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello') + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "message"=>{"dong"=>"hello"}}, c.read_message) c.close end @@ -165,12 +165,12 @@ class ClientTest < ActionCable::TestCase clients.map {|c| Concurrent::Future.execute { assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack - c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel') + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message) - c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello') + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) barrier_1.wait WAIT_WHEN_EXPECTING_EVENT - c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'bulk', message: 'hello') + c.send_message command: 'message', identifier: JSON.generate(channel: '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!) @@ -185,9 +185,9 @@ class ClientTest < ActionCable::TestCase clients.map {|c| Concurrent::Future.execute { assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack - c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel') + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message) - c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello') + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) } }.each(&:wait!) @@ -199,16 +199,16 @@ class ClientTest < ActionCable::TestCase with_puma_server do |port| c = faye_client(port) assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack - c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel') + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) - c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'delay', message: 'hello') + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'delay', message: 'hello') c.close # disappear before write c = faye_client(port) assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack - c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel') + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) - c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello') + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) c.close # disappear before read end @@ -217,7 +217,7 @@ class ClientTest < ActionCable::TestCase def test_unsubscribe_client with_puma_server do |port| app = ActionCable.server - identifier = JSON.dump(channel: 'EchoChannel') + identifier = JSON.generate(channel: 'EchoChannel') c = faye_client(port) assert_equal({"type" => "welcome"}, c.read_message) @@ -240,7 +240,7 @@ class ClientTest < ActionCable::TestCase with_puma_server do |port| c = faye_client(port) assert_equal({"type" => "welcome"}, c.read_message) - c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel') + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) ActionCable.server.restart diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb index c3d5f1f90b..b48d9af809 100644 --- a/actioncable/test/connection/identifier_test.rb +++ b/actioncable/test/connection/identifier_test.rb @@ -40,8 +40,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase open_connection_with_stubbed_pubsub @connection.websocket.expects(:close) - message = ActiveSupport::JSON.encode('type' => 'disconnect') - @connection.process_internal_message message + @connection.process_internal_message 'type' => 'disconnect' end end @@ -50,8 +49,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase open_connection_with_stubbed_pubsub @connection.websocket.expects(:close).never - message = ActiveSupport::JSON.encode('type' => 'unknown') - @connection.process_internal_message message + @connection.process_internal_message 'type' => 'unknown' end end diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb index f91597f567..53e8547245 100644 --- a/actioncable/test/connection/subscriptions_test.rb +++ b/actioncable/test/connection/subscriptions_test.rb @@ -88,7 +88,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase channel1 = subscribe_to_chat_channel - channel2_id = ActiveSupport::JSON.encode({ id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel' }) + channel2_id = ActiveSupport::JSON.encode(id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel') channel2 = subscribe_to_chat_channel(channel2_id) channel1.expects(:unsubscribe_from_channel) diff --git a/actioncable/test/stubs/test_connection.rb b/actioncable/test/stubs/test_connection.rb index 8ba284fdc6..885450dda6 100644 --- a/actioncable/test/stubs/test_connection.rb +++ b/actioncable/test/stubs/test_connection.rb @@ -3,24 +3,31 @@ require 'stubs/user' class TestConnection attr_reader :identifiers, :logger, :current_user, :server, :transmissions - def initialize(user = User.new("lifo")) + delegate :pubsub, to: :server + + def initialize(user = User.new("lifo"), coder: ActiveSupport::JSON, subscription_adapter: SuccessAdapter) + @coder = coder @identifiers = [ :current_user ] @current_user = user @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) - @server = TestServer.new + @server = TestServer.new(subscription_adapter: subscription_adapter) @transmissions = [] end - def pubsub - SuccessAdapter.new(server) + def transmit(cable_message) + @transmissions << encode(cable_message) end - def transmit(data) - @transmissions << data + def last_transmission + decode @transmissions.last if @transmissions.any? end - def last_transmission - @transmissions.last + def decode(websocket_message) + @coder.decode websocket_message + end + + def encode(cable_message) + @coder.encode cable_message end end diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb index 9e860825f3..b86f422a13 100644 --- a/actioncable/test/stubs/test_server.rb +++ b/actioncable/test/stubs/test_server.rb @@ -2,22 +2,26 @@ require 'ostruct' class TestServer include ActionCable::Server::Connections + include ActionCable::Server::Broadcasting - attr_reader :logger, :config + attr_reader :logger, :config, :mutex - def initialize + def initialize(subscription_adapter: SuccessAdapter) @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) - @config = OpenStruct.new(log_tags: [], subscription_adapter: SuccessAdapter) + + @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 def pubsub - @config.subscription_adapter.new(self) + @pubsub ||= @config.subscription_adapter.new(self) end def event_loop diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb index 82f0abbbf3..285c690df0 100644 --- a/actioncable/test/subscription_adapter/common.rb +++ b/actioncable/test/subscription_adapter/common.rb @@ -20,8 +20,7 @@ module CommonSubscriptionAdapterTest end def teardown - @tx_adapter.shutdown if @tx_adapter && @tx_adapter != @rx_adapter - @rx_adapter.shutdown if @rx_adapter + [@rx_adapter, @tx_adapter].uniq.each(&:shutdown) end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index 030362d512..de1ee96770 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -2,11 +2,13 @@ require 'action_cable' require 'active_support/testing/autorun' require 'puma' - require 'mocha/setup' - require 'rack/mock' -require 'active_support/core_ext/hash/indifferent_access' + +begin + require 'byebug' +rescue LoadError +end # Require all the stubs and models Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file } diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index ecc1e764a5..a1901e8a17 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,9 @@ +* Deprecate `datetime_field` and `datetime_field_tag` helpers. + Datetime input type was removed from HTML specification. + One can use `datetime_local_field` and `datetime_local_field_tag` instead. + + *Wojciech Wnętrzak* + * Added log "Rendering ...", when starting to render a template to log that we have started rendering something. This helps to easily identify the origin of queries in the log whether they came from controller or views. diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index e91b3443c5..7ced37572e 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1118,6 +1118,10 @@ module ActionView # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> # def datetime_field(object_name, method, options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + datetime_field is deprecated and will be removed in Rails 5.1. + Use datetime_local_field instead. + MESSAGE Tags::DatetimeField.new(object_name, method, self, options).render end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 82e9ace428..cfff0bef5d 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -691,6 +691,10 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def datetime_field_tag(name, value = nil, options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + datetime_field_tag is deprecated and will be removed in Rails 5.1. + Use datetime_local_field_tag instead. + MESSAGE text_field_tag(name, value, options.merge(type: :datetime)) end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index e77183e39f..310d0ce514 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -1139,13 +1139,17 @@ class FormHelperTest < ActionView::TestCase def test_datetime_field expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />} - assert_dom_equal(expected, datetime_field("post", "written_on")) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on")) + end end def test_datetime_field_with_datetime_value expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) - assert_dom_equal(expected, datetime_field("post", "written_on")) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on")) + end end def test_datetime_field_with_extra_attrs @@ -1154,20 +1158,26 @@ class FormHelperTest < ActionView::TestCase min_value = DateTime.new(2000, 6, 15, 20, 45, 30) max_value = DateTime.new(2010, 8, 15, 10, 25, 00) step = 60 - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step)) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step)) + end end def test_datetime_field_with_value_attr expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />} value = DateTime.new(2013,6,29,13,37) - assert_dom_equal(expected, datetime_field("post", "written_on", value: value)) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on", value: value)) + end end def test_datetime_field_with_timewithzone_value previous_time_zone, Time.zone = Time.zone, 'UTC' expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />} @post.written_on = Time.zone.parse('2004-06-15 15:30:45') - assert_dom_equal(expected, datetime_field("post", "written_on")) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on")) + end ensure Time.zone = previous_time_zone end @@ -1175,7 +1185,9 @@ class FormHelperTest < ActionView::TestCase def test_datetime_field_with_nil_value expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />} @post.written_on = nil - assert_dom_equal(expected, datetime_field("post", "written_on")) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on")) + end end def test_datetime_field_with_string_values_for_min_and_max @@ -1183,7 +1195,9 @@ class FormHelperTest < ActionView::TestCase @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) min_value = "2000-06-15T20:45:30.000+0000" max_value = "2010-08-15T10:25:00.000+0000" - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + end end def test_datetime_field_with_invalid_string_values_for_min_and_max @@ -1191,7 +1205,9 @@ class FormHelperTest < ActionView::TestCase @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) min_value = "foo" max_value = "bar" - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + end end def test_datetime_local_field diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 07b3fba754..7b93c8dc29 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -622,7 +622,9 @@ class FormTagHelperTest < ActionView::TestCase def test_datetime_field_tag expected = %{<input id="appointment" name="appointment" type="datetime" />} - assert_dom_equal(expected, datetime_field_tag("appointment")) + assert_deprecated do + assert_dom_equal(expected, datetime_field_tag("appointment")) + end end def test_datetime_local_field_tag diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 48437a1c9e..882f1225fc 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -64,11 +64,11 @@ module ActiveRecord foreign_key = join_keys.foreign_key value = transform_value(owner[foreign_key]) - scope = scope.where(table.name => { key => value }) + scope = scope.where(table.name.to_sym => { key => value }) if reflection.type polymorphic_type = transform_value(owner.class.base_class.name) - scope = scope.where(table.name => { reflection.type => polymorphic_type }) + scope = scope.where(table.name.to_sym => { reflection.type => polymorphic_type }) end scope diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 7e3760d34b..2eeefb13d7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -82,7 +82,7 @@ module ActiveRecord # Quotes the column name. Defaults to no quoting. def quote_column_name(column_name) - column_name + column_name.to_s end # Quotes the table name. Defaults to column name quoting. diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 6704843c07..069346253a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -106,6 +106,7 @@ module ActiveRecord @schema_cache = SchemaCache.new self @visitor = nil @prepared_statements = false + @quoted_column_names, @quoted_table_names = {}, {} end class Version 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 4b6289ec98..8015d1ed9e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -56,7 +56,6 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super(connection, logger, config) - @quoted_column_names, @quoted_table_names = {}, {} @visitor = Arel::Visitors::MySQL.new self @@ -165,15 +164,9 @@ module ActiveRecord raise NotImplementedError end + #-- # QUOTING ================================================== - - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`" - end - - def quote_table_name(name) #:nodoc: - @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') - end + #++ def quoted_true QUOTED_TRUE diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index 8c1442eef7..7c5980da2a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -2,6 +2,14 @@ module ActiveRecord module ConnectionAdapters module MySQL module Quoting # :nodoc: + def quote_column_name(name) + @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" + end + + def quote_table_name(name) + @quoted_table_names[name] ||= super.gsub('.', '`.`') + end + private def _quote(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index c1c77a967e..6414459cd1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -27,8 +27,8 @@ module ActiveRecord # - schema_name."table.name" # - "schema.name".table_name # - "schema.name"."table.name" - def quote_table_name(name) - Utils.extract_schema_qualified_name(name.to_s).quoted + def quote_table_name(name) # :nodoc: + @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted end # Quotes schema names for use in SQL queries. @@ -41,8 +41,8 @@ module ActiveRecord end # Quotes column names for use in SQL queries. - def quote_column_name(name) #:nodoc: - PGconn.quote_ident(name.to_s) + def quote_column_name(name) # :nodoc: + @quoted_column_names[name] ||= PGconn.quote_ident(super) end # Quote date/time values for use in SQL input. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index 3bfd0ba055..faf2f375dc 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -2,6 +2,10 @@ module ActiveRecord module ConnectionAdapters module SQLite3 module Quoting # :nodoc: + def quote_column_name(name) + @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") + end + private def _quote(value) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e585fc10fe..5c8e428bef 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -87,7 +87,6 @@ module ActiveRecord @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @visitor = Arel::Visitors::SQLite.new self - @quoted_column_names = {} if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -187,10 +186,6 @@ module ActiveRecord quote_column_name(attr) end - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}") - end - #-- # DATABASE STATEMENTS ====================================== #++ diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 86ec8000fb..c8343dd97f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -136,7 +136,7 @@ module ActiveRecord end def initialize_find_by_cache # :nodoc: - @find_by_statement_cache = {}.extend(Mutex_m) + @find_by_statement_cache = { true => {}.extend(Mutex_m), false => {}.extend(Mutex_m) } end def inherited(child_class) # :nodoc: @@ -280,8 +280,9 @@ module ActiveRecord private def cached_find_by_statement(key, &block) # :nodoc: - @find_by_statement_cache[key] || @find_by_statement_cache.synchronize { - @find_by_statement_cache[key] ||= StatementCache.create(connection, &block) + cache = @find_by_statement_cache[connection.prepared_statements] + cache[key] || cache.synchronize { + cache[key] ||= StatementCache.create(connection, &block) } end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 52eab952e1..4c836378e0 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -144,6 +144,13 @@ module ActiveRecord def table_name=(value) value = value && value.to_s + if value && value.include?(".") + # When this deprecation warning is removed, revert commit 04ac5655be91f49cd4dfe2838df96213502fb274 + ActiveSupport::Deprecation.warn( + 'Support for table names containing "." is deprecated and will be removed in Rails 5.1.' + ) + end + if defined?(@table_name) return if value == @table_name reset_column_information if connected? diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 953495a8b6..550416238f 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -84,6 +84,7 @@ module ActiveRecord return ["1=0"] if attributes.empty? attributes.flat_map do |key, value| + key = key.to_s if value.is_a?(Hash) associated_predicate_builder(key).expand_from_hash(value) else @@ -136,7 +137,9 @@ module ActiveRecord end def convert_dot_notation_to_hash(attributes) - dot_notation = attributes.keys.select { |s| s.include?(".".freeze) } + dot_notation = attributes.keys.select do |s| + s.respond_to?(:include?) && s.include?(".".freeze) + end dot_notation.each do |key| table_name, column_name = key.split(".".freeze) diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index dbf172a577..c0ccb00b6f 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -15,7 +15,6 @@ module ActiveRecord when Hash attributes = predicate_builder.resolve_column_aliases(opts) attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) - attributes.stringify_keys! attributes, binds = predicate_builder.create_binds(attributes) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index eef2d29d02..24746ea56d 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1531,4 +1531,12 @@ class BasicsTest < ActiveRecord::TestCase assert Developer.new.respond_to?(:last_name=) assert Developer.new.respond_to?(:last_name?) end + + test "table names containing a dot are deprecated" do + assert_deprecated do + Class.new(ActiveRecord::Base) do + self.table_name = "foo.bar" + end + end + end end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index a704b861cb..104226010a 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -94,5 +94,17 @@ module ActiveRecord additional_books = cache.execute([], Book, Book.connection) assert first_books != additional_books end + + def test_unprepared_statements_dont_share_a_cache_with_prepared_statements + Book.create(name: "my book") + Book.create(name: "my other book") + + book = Book.find_by(name: "my book") + other_book = Book.connection.unprepared_statement do + Book.find_by(name: "my other book") + end + + refute_equal book, other_book + end end end diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 8997363fce..848c9caa59 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -1088,6 +1088,8 @@ You can filter out sensitive request parameters from your log files by appending config.filter_parameters << :password ``` +NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`. + ### Redirects Filtering Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index e57d873719..e80f994deb 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -104,7 +104,7 @@ application. Accepts a valid week day symbol (e.g. `:monday`). * `config.filter_parameters` used for filtering out the parameters that you don't want shown in the logs, such as passwords or credit card -numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`. +numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression. * `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. diff --git a/guides/source/security.md b/guides/source/security.md index 4883c1abe7..16c5291037 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -494,6 +494,8 @@ By default, Rails logs all requests being made to the web application. But log f config.filter_parameters << :password ``` +NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`. + ### Good Passwords INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._ |