diff options
40 files changed, 558 insertions, 398 deletions
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 1167387def..0b418c4ea1 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -11,7 +11,7 @@ end require 'minitest/autorun' require 'action_mailer' require 'action_mailer/test_case' -require 'rails/queueing' +require 'active_support/queueing' silence_warnings do # These external dependencies have warnings :/ @@ -27,7 +27,7 @@ ActionView::Template.register_template_handler :bak, lambda { |template| "Lame b FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__)) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -ActionMailer::Base.queue = Rails::Queueing::SynchronousQueue.new +ActionMailer::Base.queue = ActiveSupport::SynchronousQueue.new class MockSMTP def self.deliveries diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 862b954f9e..6a06cec041 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -3,13 +3,13 @@ require 'abstract_unit' require 'set' require 'action_dispatch' +require 'active_support/queueing' require 'active_support/time' require 'mailers/base_mailer' require 'mailers/proc_mailer' require 'mailers/asset_mailer' require 'mailers/async_mailer' -require 'rails/queueing' class BaseTest < ActiveSupport::TestCase def teardown @@ -433,7 +433,7 @@ class BaseTest < ActiveSupport::TestCase end test "delivering message asynchronously" do - testing_queue = Rails::Queueing::TestQueue.new + testing_queue = ActiveSupport::TestQueue.new AsyncMailer.delivery_method = :test AsyncMailer.deliveries.clear stub_queue(AsyncMailer, testing_queue).welcome.deliver diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 36e8479441..239e4445d3 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,23 @@ ## Rails 4.0.0 (unreleased) ## +* Support multiple etags in If-None-Match header. *Travis Warlick* + +* Allow to configure how unverified request will be handled using `:with` + option in `protect_from_forgery` method. + + Valid unverified request handling methods are: + + - `:exception` - Raises ActionController::InvalidAuthenticityToken exception. + - `:reset_session` - Resets the session. + - `:null_session` - Provides an empty session during request but doesn't + reset it completely. Used as default if `:with` option is not specified. + + New applications are generated with: + + protect_from_forgery :with => :exception + + *Sergey Nartimov* + * Add .rb template handler, this handler simply allows arbitrary Ruby code as a template. *Guillermo Iguaran* * Add `separator` option for `ActionView::Helpers::TextHelper#excerpt`: diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 81188ac8cc..17d4a793ac 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,3 +1,4 @@ +require 'rack/session/abstract/id' require 'action_controller/metal/exceptions' module ActionController #:nodoc: diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index a7f93b780e..13e2701663 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -17,12 +17,18 @@ module ActionDispatch env[HTTP_IF_NONE_MATCH] end + def if_none_match_etags + (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag| + etag.gsub(/^\"|\"$/, "") + end + end + def not_modified?(modified_at) if_modified_since && modified_at && if_modified_since >= modified_at end def etag_matches?(etag) - if_none_match && if_none_match == etag + if_none_match_etags.include?(etag) end # Check response freshness (Last-Modified and ETag) against request diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index af367ac28d..76f4dea7b8 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -149,7 +149,7 @@ module ActionView # as well as incorrectly putting part of the path in the template # name instead of the prefix. def normalize_name(name, prefixes) #:nodoc: - prefixes = nil if prefixes.blank? + prefixes = prefixes.presence parts = name.to_s.split('/') parts.shift if parts.first.empty? name = parts.pop diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index a434e49dbd..a2b9571660 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -746,6 +746,45 @@ class RequestTest < ActiveSupport::TestCase assert_equal "/foo?bar", path end + test "if_none_match_etags none" do + request = stub_request + + assert_equal nil, request.if_none_match + assert_equal [], request.if_none_match_etags + assert !request.etag_matches?("foo") + assert !request.etag_matches?(nil) + end + + test "if_none_match_etags single" do + header = 'the-etag' + request = stub_request('HTTP_IF_NONE_MATCH' => header) + + assert_equal header, request.if_none_match + assert_equal [header], request.if_none_match_etags + assert request.etag_matches?("the-etag") + end + + test "if_none_match_etags quoted single" do + header = '"the-etag"' + request = stub_request('HTTP_IF_NONE_MATCH' => header) + + assert_equal header, request.if_none_match + assert_equal ['the-etag'], request.if_none_match_etags + assert request.etag_matches?("the-etag") + end + + test "if_none_match_etags multiple" do + header = 'etag1, etag2, "third etag", "etag4"' + expected = ['etag1', 'etag2', 'third etag', 'etag4'] + request = stub_request('HTTP_IF_NONE_MATCH' => header) + + assert_equal header, request.if_none_match + assert_equal expected, request.if_none_match_etags + expected.each do |etag| + assert request.etag_matches?(etag), etag + end + end + protected def stub_request(env = {}) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 11e4d34de2..32e3c7f5d8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -3,8 +3,7 @@ module ActiveRecord module DatabaseStatements def initialize super - @_current_transaction_records = [] - @transaction_joinable = nil + reset_transaction end # Converts an arel AST to SQL @@ -108,20 +107,6 @@ module ActiveRecord exec_delete(to_sql(arel, binds), name, binds) end - # Checks whether there is currently no transaction active. This is done - # by querying the database driver, and does not use the transaction - # house-keeping information recorded by #increment_open_transactions and - # friends. - # - # Returns true if there is no transaction active, false if there is a - # transaction active, and nil if this information is unknown. - # - # Not all adapters supports transaction state introspection. Currently, - # only the PostgreSQL adapter supports this. - def outside_transaction? - nil - end - # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -173,76 +158,60 @@ module ActiveRecord def transaction(options = {}) options.assert_valid_keys :requires_new, :joinable - last_transaction_joinable = @transaction_joinable - @transaction_joinable = options.fetch(:joinable, true) - requires_new = options[:requires_new] || !last_transaction_joinable - transaction_open = false - - begin - if requires_new || open_transactions == 0 - if open_transactions == 0 - begin_db_transaction - elsif requires_new - create_savepoint - end - increment_open_transactions - transaction_open = true - @_current_transaction_records.push([]) - end + if !options[:requires_new] && current_transaction.joinable? yield - rescue Exception => database_transaction_rollback - if transaction_open && !outside_transaction? - transaction_open = false - txn = decrement_open_transactions - txn.aborted! - if open_transactions == 0 - rollback_db_transaction - rollback_transaction_records(true) - else - rollback_to_savepoint - rollback_transaction_records(false) - end - end - raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback) + else + within_new_transaction(options) { yield } end + rescue ActiveRecord::Rollback + # rollbacks are silently swallowed + end + + def within_new_transaction(options = {}) #:nodoc: + begin_transaction(options) + yield + rescue Exception => error + rollback_transaction + raise ensure - @transaction_joinable = last_transaction_joinable - - if outside_transaction? - @current_transaction = nil - elsif transaction_open - txn = decrement_open_transactions - txn.committed! - begin - if open_transactions == 0 - commit_db_transaction - commit_transaction_records - else - release_savepoint - save_point_records = @_current_transaction_records.pop - unless save_point_records.blank? - @_current_transaction_records.push([]) if @_current_transaction_records.empty? - @_current_transaction_records.last.concat(save_point_records) - end - end - rescue Exception - if open_transactions == 0 - rollback_db_transaction - rollback_transaction_records(true) - else - rollback_to_savepoint - rollback_transaction_records(false) - end - raise - end + begin + commit_transaction unless error + rescue Exception + rollback_transaction + raise end end + def current_transaction #:nodoc: + @transaction + end + + def transaction_open? + @transaction.open? + end + + def begin_transaction(options = {}) #:nodoc: + @transaction = @transaction.begin + @transaction.joinable = options.fetch(:joinable, true) + @transaction + end + + def commit_transaction #:nodoc: + @transaction = @transaction.commit + end + + def rollback_transaction #:nodoc: + @transaction = @transaction.rollback + end + + def reset_transaction #:nodoc: + @transaction = ClosedTransaction.new(self) + end + # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - last_batch = @_current_transaction_records.last - last_batch << record if last_batch + @transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). @@ -356,42 +325,6 @@ module ActiveRecord update_sql(sql, name) end - # Send a rollback message to all records after they have been rolled back. If rollback - # is false, only rollback records since the last save point. - def rollback_transaction_records(rollback) - if rollback - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - else - records = @_current_transaction_records.pop - end - - unless records.blank? - records.uniq.each do |record| - begin - record.rolledback!(rollback) - rescue => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end - end - end - end - - # Send a commit message to all records after they have been committed. - def commit_transaction_records - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - unless records.blank? - records.uniq.each do |record| - begin - record.committed! - rescue => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end - end - end - end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) [sql, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb new file mode 100644 index 0000000000..2117eae5cb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -0,0 +1,156 @@ +module ActiveRecord + module ConnectionAdapters + class Transaction #:nodoc: + attr_reader :connection + + def initialize(connection) + @connection = connection + end + end + + class ClosedTransaction < Transaction #:nodoc: + def number + 0 + end + + def begin + RealTransaction.new(connection, self) + end + + def closed? + true + end + + def open? + false + end + + def joinable? + false + end + + # This is a noop when there are no open transactions + def add_record(record) + end + end + + class OpenTransaction < Transaction #:nodoc: + attr_reader :parent, :records + attr_writer :joinable + + def initialize(connection, parent) + super connection + + @parent = parent + @records = [] + @finishing = false + @joinable = true + end + + # This state is necesarry so that we correctly handle stuff that might + # happen in a commit/rollback. But it's kinda distasteful. Maybe we can + # find a better way to structure it in the future. + def finishing? + @finishing + end + + def joinable? + @joinable && !finishing? + end + + def number + if finishing? + parent.number + else + parent.number + 1 + end + end + + def begin + if finishing? + parent.begin + else + SavepointTransaction.new(connection, self) + end + end + + def rollback + @finishing = true + perform_rollback + parent + end + + def commit + @finishing = true + perform_commit + parent + end + + def add_record(record) + records << record + end + + def rollback_records + records.uniq.each do |record| + begin + record.rolledback!(parent.closed?) + rescue => e + record.logger.error(e) if record.respond_to?(:logger) && record.logger + end + end + end + + def commit_records + records.uniq.each do |record| + begin + record.committed! + rescue => e + record.logger.error(e) if record.respond_to?(:logger) && record.logger + end + end + end + + def closed? + false + end + + def open? + true + end + end + + class RealTransaction < OpenTransaction #:nodoc: + def initialize(connection, parent) + super + connection.begin_db_transaction + end + + def perform_rollback + connection.rollback_db_transaction + rollback_records + end + + def perform_commit + connection.commit_db_transaction + commit_records + end + end + + class SavepointTransaction < OpenTransaction #:nodoc: + def initialize(connection, parent) + super + connection.create_savepoint + end + + def perform_rollback + connection.rollback_to_savepoint + rollback_records + end + + def perform_commit + connection.release_savepoint + records.each { |r| parent.add_record(r) } + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 27700e4fd2..c37c9b1ae1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' require 'monitor' +require 'active_support/deprecation' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -33,6 +34,12 @@ module ActiveRecord autoload :QueryCache end + autoload_at 'active_record/connection_adapters/abstract/transaction' do + autoload :ClosedTransaction + autoload :RealTransaction + autoload :SavepointTransaction + end + # Active Record supports multiple database systems. AbstractAdapter and # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an @@ -62,14 +69,11 @@ module ActiveRecord def initialize(connection, logger = nil, pool = nil) #:nodoc: super() - @active = nil @connection = connection @in_use = false @instrumenter = ActiveSupport::Notifications.instrumenter @last_use = false @logger = logger - @open_transactions = 0 - @current_transaction = nil @pool = pool @query_cache = Hash.new { |h,sql| h[sql] = {} } @query_cache_enabled = false @@ -182,19 +186,21 @@ module ActiveRecord # checking whether the database is actually capable of responding, i.e. whether # the connection isn't stale. def active? - @active != false end # Disconnects from the database if already connected, and establishes a - # new connection with the database. + # new connection with the database. Implementors should call super if they + # override the default implementation. def reconnect! - @active = true + clear_cache! + reset_transaction end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - @active = false + clear_cache! + reset_transaction end # Reset the state of this connection, directing the DBMS to clear @@ -238,33 +244,20 @@ module ActiveRecord end def open_transactions - count = 0 - txn = current_transaction - - while txn - count += 1 - txn = txn.next - end - - count + @transaction.number end - attr_reader :current_transaction - def increment_open_transactions - @current_transaction = Transaction.new(current_transaction) + ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect" end def decrement_open_transactions - return unless current_transaction - - txn = current_transaction - @current_transaction = txn.next - txn + ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect" end def transaction_joinable=(joinable) - @transaction_joinable = joinable + ActiveSupport::Deprecation.warn "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead." + @transaction.joinable = joinable end def create_savepoint diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 8fc172f6e8..328d080687 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -74,6 +74,7 @@ module ActiveRecord end def reconnect! + super disconnect! connect end @@ -82,6 +83,7 @@ module ActiveRecord # Disconnects from the database if already connected. # Otherwise, this method does nothing. def disconnect! + super unless @connection.nil? @connection.close @connection = nil diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index bb63fddf9b..0b936bbf39 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -189,14 +189,15 @@ module ActiveRecord end def reconnect! + super disconnect! - clear_cache! connect end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! + super @connection.close rescue nil end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index eb3084e066..c8437c18cc 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation' + module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter < AbstractAdapter @@ -214,6 +216,10 @@ module ActiveRecord end def outside_transaction? + ActiveSupport::Deprecation.warn( + "#outside_transaction? is deprecated. This method was only really used " \ + "internally, but you can use #transaction_open? instead." + ) @connection.transaction_status == PGconn::PQTRANS_IDLE end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index d1751d70c6..e85e63d607 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -431,9 +431,8 @@ module ActiveRecord # Close then reopen the connection. def reconnect! - clear_cache! + super @connection.reset - @open_transactions = 0 configure_connection end @@ -445,7 +444,7 @@ module ActiveRecord # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - clear_cache! + super @connection.close rescue nil end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 4fe0013f0f..b6dd2e17f4 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -104,6 +104,8 @@ module ActiveRecord def initialize(connection, logger, config) super(connection, logger) + + @active = nil @statements = StatementPool.new(@connection, config.fetch(:statement_limit) { 1000 }) @config = config @@ -154,11 +156,15 @@ module ActiveRecord true end + def active? + @active != false + end + # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! super - clear_cache! + @active = false @connection.close rescue nil end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index cf64985ddb..43ffca0227 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -387,7 +387,6 @@ module ActiveRecord @marked_for_destruction = false @new_record = true @mass_assignment_options = nil - @txn = nil @_start_transaction_state = {} end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b1db5f6f9f..60fc653735 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -843,9 +843,7 @@ module ActiveRecord end @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| - connection.increment_open_transactions - connection.transaction_joinable = false - connection.begin_db_transaction + connection.begin_transaction joinable: false end # Load fixtures for every test. else @@ -868,10 +866,7 @@ module ActiveRecord # Rollback changes if a transaction is active. if run_in_transaction? @fixture_connections.each do |connection| - if connection.open_transactions != 0 - connection.rollback_db_transaction - connection.decrement_open_transactions - end + connection.rollback_transaction if connection.transaction_open? end @fixture_connections.clear end diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb index 65a3d68619..90b462fad6 100644 --- a/activerecord/lib/active_record/railties/console_sandbox.rb +++ b/activerecord/lib/active_record/railties/console_sandbox.rb @@ -1,6 +1,4 @@ -ActiveRecord::Base.connection.increment_open_transactions ActiveRecord::Base.connection.begin_db_transaction at_exit do ActiveRecord::Base.connection.rollback_db_transaction - ActiveRecord::Base.connection.decrement_open_transactions end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index e008b32170..09318879d5 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -1,32 +1,6 @@ require 'thread' module ActiveRecord - class Transaction - attr_reader :next - - def initialize(txn = nil) - @next = txn - @committed = false - @aborted = false - end - - def committed! - @committed = true - end - - def aborted! - @aborted = true - end - - def committed? - @committed - end - - def aborted? - @aborted - end - end - # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions extend ActiveSupport::Concern @@ -333,11 +307,11 @@ module ActiveRecord def with_transaction_returning_status status = nil self.class.transaction do - @txn = self.class.connection.current_transaction add_to_transaction begin status = yield rescue ActiveRecord::Rollback + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 status = nil end @@ -353,17 +327,20 @@ module ActiveRecord @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record @_start_transaction_state[:destroyed] = @destroyed + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: - @_start_transaction_state.clear if @txn.committed? + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force = false) #:nodoc: unless @_start_transaction_state.empty? - if @txn.aborted? || force + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + if @_start_transaction_state[:level] < 1 || force restore_state = @_start_transaction_state was_frozen = @attributes.frozen? @attributes = @attributes.dup if was_frozen diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 1199be68eb..59324c4857 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -36,6 +36,10 @@ module ActiveRecord def columns(table_name) @columns[table_name] end + + def active? + true + end end end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 852fc0e26e..93b01a3934 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -160,4 +160,36 @@ module ActiveRecord end end end + + class AdapterTestWithoutTransaction < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def setup + @klass = Class.new(ActiveRecord::Base) + @klass.establish_connection 'arunit' + @connection = @klass.connection + end + + def teardown + @klass.remove_connection + end + + test "transaction state is reset after a reconnect" do + skip "in-memory db doesn't allow reconnect" if in_memory_db? + + @connection.begin_transaction + assert @connection.transaction_open? + @connection.reconnect! + assert !@connection.transaction_open? + end + + test "transaction state is reset after a disconnect" do + skip "in-memory db doesn't allow disconnect" if in_memory_db? + + @connection.begin_transaction + assert @connection.transaction_open? + @connection.disconnect! + assert !@connection.transaction_open? + end + end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 0d0de455b3..0b5fda3817 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -36,6 +36,7 @@ class TransactionTest < ActiveRecord::TestCase end end + # FIXME: Get rid of this fucking global variable! def test_successful_with_return class << Topic.connection alias :real_commit_db_transaction :commit_db_transaction @@ -348,7 +349,6 @@ class TransactionTest < ActiveRecord::TestCase def test_rollback_when_commit_raises Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:commit_db_transaction).raises('OH NOES') - Topic.connection.expects(:outside_transaction?).returns(false) Topic.connection.expects(:rollback_db_transaction) assert_raise RuntimeError do @@ -397,31 +397,11 @@ class TransactionTest < ActiveRecord::TestCase if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE) def test_outside_transaction_works - assert Topic.connection.outside_transaction? + assert assert_deprecated { Topic.connection.outside_transaction? } Topic.connection.begin_db_transaction - assert !Topic.connection.outside_transaction? + assert assert_deprecated { !Topic.connection.outside_transaction? } Topic.connection.rollback_db_transaction - assert Topic.connection.outside_transaction? - end - - def test_rollback_wont_be_executed_if_no_transaction_active - assert_raise RuntimeError do - Topic.transaction do - Topic.connection.rollback_db_transaction - Topic.connection.expects(:rollback_db_transaction).never - raise "Rails doesn't scale!" - end - end - end - - def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active - Topic.transaction do - Topic.transaction do - Topic.connection.rollback_db_transaction - end - assert_equal 0, Topic.connection.open_transactions - end - assert_equal 0, Topic.connection.open_transactions + assert assert_deprecated { Topic.connection.outside_transaction? } end end @@ -580,5 +560,14 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal original_salary, Developer.find(1).salary end + + test "#transaction_joinable= is deprecated" do + Developer.transaction do + conn = Developer.connection + assert conn.current_transaction.joinable? + assert_deprecated { conn.transaction_joinable = false } + assert !conn.current_transaction.joinable? + end + end end end diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb index e7dc60a612..83cc8066e7 100644 --- a/activesupport/lib/active_support/core_ext/object/to_json.rb +++ b/activesupport/lib/active_support/core_ext/object/to_json.rb @@ -17,3 +17,11 @@ end end end end + +module Process + class Status + def as_json(options = nil) + { :exitstatus => exitstatus, :pid => pid } + end + end +end diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 02e8ff7c87..43a4ed81e5 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -35,8 +35,8 @@ module ActiveSupport end def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = caller) - deprecated_method_warning(deprecated_method_name, message).tap do |message| - warn(message, caller_backtrace) + deprecated_method_warning(deprecated_method_name, message).tap do |msg| + warn(msg, caller_backtrace) end end diff --git a/activesupport/lib/active_support/queueing.rb b/activesupport/lib/active_support/queueing.rb new file mode 100644 index 0000000000..f397e1c0c5 --- /dev/null +++ b/activesupport/lib/active_support/queueing.rb @@ -0,0 +1,116 @@ +require 'delegate' +require 'thread' + +module ActiveSupport + # A Queue that simply inherits from STDLIB's Queue. Everytime this + # queue is used, Rails automatically sets up a ThreadedConsumer + # to consume it. + class Queue < ::Queue + end + + class SynchronousQueue < ::Queue + def push(job) + result = nil + Thread.new { result = job.run }.join + result + end + alias << push + alias enq push + end + + # In test mode, the Rails queue is backed by an Array so that assertions + # can be made about its contents. The test queue provides a +jobs+ + # method to make assertions about the queue's contents and a +drain+ + # method to drain the queue and run the jobs. + # + # Jobs are run in a separate thread to catch mistakes where code + # assumes that the job is run in the same thread. + class TestQueue < ::Queue + # Get a list of the jobs off this queue. This method may not be + # available on production queues. + def jobs + @que.dup + end + + # Marshal and unmarshal job before pushing it onto the queue. This will + # raise an exception on any attempts in tests to push jobs that can't (or + # shouldn't) be marshalled. + def push(job) + super Marshal.load(Marshal.dump(job)) + end + + # Drain the queue, running all jobs in a different thread. This method + # may not be available on production queues. + def drain + # run the jobs in a separate thread so assumptions of synchronous + # jobs are caught in test mode. + Thread.new { pop.run until empty? }.join + end + end + + # A container for multiple queues. This class delegates to a default Queue + # so that <tt>Rails.queue.push</tt> and friends will Just Work. To use this class + # with multiple queues: + # + # # In your configuration: + # Rails.queue[:image_queue] = SomeQueue.new + # Rails.queue[:mail_queue] = SomeQueue.new + # + # # In your app code: + # Rails.queue[:mail_queue].push SomeJob.new + # + class QueueContainer < DelegateClass(::Queue) + def initialize(default_queue) + @queues = { :default => default_queue } + super(default_queue) + end + + def [](queue_name) + @queues[queue_name] + end + + def []=(queue_name, queue) + @queues[queue_name] = queue + end + end + + # The threaded consumer will run jobs in a background thread in + # development mode or in a VM where running jobs on a thread in + # production mode makes sense. + # + # When the process exits, the consumer pushes a nil onto the + # queue and joins the thread, which will ensure that all jobs + # are executed before the process finally dies. + class ThreadedQueueConsumer + def self.start(queue, logger=nil) + new(queue, logger).start + end + + def initialize(queue, logger=nil) + @queue = queue + @logger = logger + end + + def start + @thread = Thread.new do + while job = @queue.pop + begin + job.run + rescue Exception => e + handle_exception e + end + end + end + self + end + + def shutdown + @queue.push nil + @thread.join + end + + def handle_exception(e) + @logger.error "Job Error: #{e.message}\n#{e.backtrace.join("\n")}" if @logger + end + end +end diff --git a/activesupport/test/queueing/container_test.rb b/activesupport/test/queueing/container_test.rb new file mode 100644 index 0000000000..7afc11e7a9 --- /dev/null +++ b/activesupport/test/queueing/container_test.rb @@ -0,0 +1,28 @@ +require 'abstract_unit' +require 'active_support/queueing' + +module ActiveSupport + class ContainerTest < ActiveSupport::TestCase + def test_delegates_to_default + q = Queue.new + container = QueueContainer.new q + job = Object.new + + container.push job + assert_equal job, q.pop + end + + def test_access_default + q = Queue.new + container = QueueContainer.new q + assert_equal q, container[:default] + end + + def test_assign_queue + container = QueueContainer.new Object.new + q = Object.new + container[:foo] = q + assert_equal q, container[:foo] + end + end +end diff --git a/railties/test/queueing/test_queue_test.rb b/activesupport/test/queueing/test_queue_test.rb index 2f0f507adb..4c08314366 100644 --- a/railties/test/queueing/test_queue_test.rb +++ b/activesupport/test/queueing/test_queue_test.rb @@ -1,9 +1,9 @@ require 'abstract_unit' -require 'rails/queueing' +require 'active_support/queueing' class TestQueueTest < ActiveSupport::TestCase def setup - @queue = Rails::Queueing::TestQueue.new + @queue = ActiveSupport::TestQueue.new end class ExceptionRaisingJob diff --git a/railties/test/queueing/threaded_consumer_test.rb b/activesupport/test/queueing/threaded_consumer_test.rb index c34a966d6e..20a1cc4e8e 100644 --- a/railties/test/queueing/threaded_consumer_test.rb +++ b/activesupport/test/queueing/threaded_consumer_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' -require 'rails/queueing' +require 'active_support/queueing' +require "active_support/log_subscriber/test_helper" class TestThreadConsumer < ActiveSupport::TestCase class Job @@ -15,8 +16,9 @@ class TestThreadConsumer < ActiveSupport::TestCase end def setup - @queue = Rails::Queueing::Queue.new - @consumer = Rails::Queueing::ThreadedConsumer.start(@queue) + @queue = ActiveSupport::Queue.new + @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + @consumer = ActiveSupport::ThreadedQueueConsumer.start(@queue, @logger) end def teardown @@ -64,10 +66,6 @@ class TestThreadConsumer < ActiveSupport::TestCase end test "log job that raises an exception" do - require "active_support/log_subscriber/test_helper" - logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new - Rails.logger = logger - job = Job.new(1) do raise "RuntimeError: Error!" end @@ -75,13 +73,13 @@ class TestThreadConsumer < ActiveSupport::TestCase @queue.push job sleep 0.1 - assert_equal 1, logger.logged(:error).size - assert_match(/Job Error: RuntimeError: Error!/, logger.logged(:error).last) + assert_equal 1, @logger.logged(:error).size + assert_match(/Job Error: RuntimeError: Error!/, @logger.logged(:error).last) end test "test overriding exception handling" do @consumer.shutdown - @consumer = Class.new(Rails::Queueing::ThreadedConsumer) do + @consumer = Class.new(ActiveSupport::ThreadedQueueConsumer) do attr_reader :last_error def handle_exception(e) @last_error = e.message diff --git a/guides/source/configuring.textile b/guides/source/configuring.textile index 73d6102eac..9c35738bf8 100644 --- a/guides/source/configuring.textile +++ b/guides/source/configuring.textile @@ -111,9 +111,9 @@ end * +config.middleware+ allows you to configure the application's middleware. This is covered in depth in the "Configuring Middleware":#configuring-middleware section below. -* +config.queue+ configures a different queue implementation for the application. Defaults to +Rails::Queueing::Queue+. Note that, if the default queue is changed, the default +queue_consumer+ is not going to be initialized, it is up to the new queue implementation to handle starting and shutting down its own consumer(s). +* +config.queue+ configures a different queue implementation for the application. Defaults to +ActiveSupport::SynchronousQueue+. Note that, if the default queue is changed, the default +queue_consumer+ is not going to be initialized, it is up to the new queue implementation to handle starting and shutting down its own consumer(s). -* +config.queue_consumer+ configures a different consumer implementation for the default queue. Defaults to +Rails::Queueing::ThreadedConsumer+. +* +config.queue_consumer+ configures a different consumer implementation for the default queue. Defaults to +ActiveSupport::ThreadedQueueConsumer+. * +config.reload_classes_only_on_change+ enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If +config.cache_classes+ is true, this option is ignored. diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index 670477f91a..a15965a9da 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -22,7 +22,6 @@ end module Rails autoload :Info, 'rails/info' autoload :InfoController, 'rails/info_controller' - autoload :Queueing, 'rails/queueing' class << self def application diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 80b8e4b9ba..aa5b986862 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,4 +1,5 @@ require 'fileutils' +require 'active_support/queueing' require 'rails/engine' module Rails @@ -188,7 +189,7 @@ module Rails end def queue #:nodoc: - @queue ||= Queueing::Container.new(build_queue) + @queue ||= ActiveSupport::QueueContainer.new(build_queue) end def build_queue #:nodoc: diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 7a0bb21043..c4c1fcca40 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/kernel/reporting' require 'active_support/file_update_checker' +require 'active_support/queueing' require 'rails/engine/configuration' module Rails @@ -41,8 +42,8 @@ module Rails @exceptions_app = nil @autoflush_log = true @log_formatter = ActiveSupport::Logger::SimpleFormatter.new - @queue = Rails::Queueing::SynchronousQueue - @queue_consumer = Rails::Queueing::ThreadedConsumer + @queue = ActiveSupport::SynchronousQueue + @queue_consumer = ActiveSupport::ThreadedQueueConsumer @eager_load = nil @assets = ActiveSupport::OrderedOptions.new diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 777f0d269e..f9a3c00946 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -97,8 +97,8 @@ module Rails end initializer :activate_queue_consumer do |app| - if config.queue == Rails::Queueing::Queue - app.queue_consumer = config.queue_consumer.start(app.queue) + if config.queue == ActiveSupport::Queue + app.queue_consumer = config.queue_consumer.start(app.queue, Rails.logger) at_exit { app.queue_consumer.shutdown } end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index fdf011a510..cb3e8b123e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -82,5 +82,5 @@ # Default the production mode queue to an synchronous queue. You will probably # want to replace this with an out-of-process queueing solution. - # config.queue = Rails::Queueing::SynchronousQueue + # config.queue = ActiveSupport::SynchronousQueue end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index 8ab27eb6e1..75897ba8cd 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -40,5 +40,5 @@ config.active_support.deprecation = :stderr # Use the testing queue. - config.queue = Rails::Queueing::TestQueue + config.queue = ActiveSupport::TestQueue end diff --git a/railties/lib/rails/queueing.rb b/railties/lib/rails/queueing.rb deleted file mode 100644 index 7cd755b0f7..0000000000 --- a/railties/lib/rails/queueing.rb +++ /dev/null @@ -1,115 +0,0 @@ -require "thread" -require 'delegate' - -module Rails - module Queueing - # A container for multiple queues. This class delegates to a default Queue - # so that <tt>Rails.queue.push</tt> and friends will Just Work. To use this class - # with multiple queues: - # - # # In your configuration: - # Rails.queue[:image_queue] = SomeQueue.new - # Rails.queue[:mail_queue] = SomeQueue.new - # - # # In your app code: - # Rails.queue[:mail_queue].push SomeJob.new - # - class Container < DelegateClass(::Queue) - def initialize(default_queue) - @queues = { :default => default_queue } - super(default_queue) - end - - def [](queue_name) - @queues[queue_name] - end - - def []=(queue_name, queue) - @queues[queue_name] = queue - end - end - - # A Queue that simply inherits from STDLIB's Queue. Everytime this - # queue is used, Rails automatically sets up a ThreadedConsumer - # to consume it. - class Queue < ::Queue - end - - class SynchronousQueue < ::Queue - def push(job) - job.run - end - alias << push - alias enq push - end - - # In test mode, the Rails queue is backed by an Array so that assertions - # can be made about its contents. The test queue provides a +jobs+ - # method to make assertions about the queue's contents and a +drain+ - # method to drain the queue and run the jobs. - # - # Jobs are run in a separate thread to catch mistakes where code - # assumes that the job is run in the same thread. - class TestQueue < ::Queue - # Get a list of the jobs off this queue. This method may not be - # available on production queues. - def jobs - @que.dup - end - - # Marshal and unmarshal job before pushing it onto the queue. This will - # raise an exception on any attempts in tests to push jobs that can't (or - # shouldn't) be marshalled. - def push(job) - super Marshal.load(Marshal.dump(job)) - end - - # Drain the queue, running all jobs in a different thread. This method - # may not be available on production queues. - def drain - # run the jobs in a separate thread so assumptions of synchronous - # jobs are caught in test mode. - Thread.new { pop.run until empty? }.join - end - end - - # The threaded consumer will run jobs in a background thread in - # development mode or in a VM where running jobs on a thread in - # production mode makes sense. - # - # When the process exits, the consumer pushes a nil onto the - # queue and joins the thread, which will ensure that all jobs - # are executed before the process finally dies. - class ThreadedConsumer - def self.start(queue) - new(queue).start - end - - def initialize(queue) - @queue = queue - end - - def start - @thread = Thread.new do - while job = @queue.pop - begin - job.run - rescue Exception => e - handle_exception e - end - end - end - self - end - - def shutdown - @queue.push nil - @thread.join - end - - def handle_exception(e) - Rails.logger.error "Job Error: #{e.message}\n#{e.backtrace.join("\n")}" - end - end - end -end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 1eb5fce384..5268257d62 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -52,19 +52,19 @@ module ApplicationTests test "uses the default queue for ActionMailer" do require "#{app_path}/config/environment" - assert_kind_of Rails::Queueing::Container, ActionMailer::Base.queue + assert_kind_of ActiveSupport::QueueContainer, ActionMailer::Base.queue end test "allows me to configure queue for ActionMailer" do app_file "config/environments/development.rb", <<-RUBY AppTemplate::Application.configure do - Rails.queue[:mailer] = Rails::Queueing::TestQueue.new + Rails.queue[:mailer] = ActiveSupport::TestQueue.new config.action_mailer.queue = Rails.queue[:mailer] end RUBY require "#{app_path}/config/environment" - assert_kind_of Rails::Queueing::TestQueue, ActionMailer::Base.queue + assert_kind_of ActiveSupport::TestQueue, ActionMailer::Base.queue end test "does not include url helpers as action methods" do diff --git a/railties/test/application/queue_test.rb b/railties/test/application/queue_test.rb index f4c11c5f4f..664001ca69 100644 --- a/railties/test/application/queue_test.rb +++ b/railties/test/application/queue_test.rb @@ -19,14 +19,14 @@ module ApplicationTests test "the queue is a TestQueue in test mode" do app("test") - assert_kind_of Rails::Queueing::TestQueue, Rails.application.queue[:default] - assert_kind_of Rails::Queueing::TestQueue, Rails.queue[:default] + assert_kind_of ActiveSupport::TestQueue, Rails.application.queue[:default] + assert_kind_of ActiveSupport::TestQueue, Rails.queue[:default] end test "the queue is a SynchronousQueue in development mode" do app("development") - assert_kind_of Rails::Queueing::SynchronousQueue, Rails.application.queue[:default] - assert_kind_of Rails::Queueing::SynchronousQueue, Rails.queue[:default] + assert_kind_of ActiveSupport::SynchronousQueue, Rails.application.queue[:default] + assert_kind_of ActiveSupport::SynchronousQueue, Rails.queue[:default] end class ThreadTrackingJob @@ -47,7 +47,7 @@ module ApplicationTests end end - test "in development mode, an enqueued job will be processed in the same thread" do + test "in development mode, an enqueued job will be processed in a separate thread" do app("development") job = ThreadTrackingJob.new @@ -55,7 +55,7 @@ module ApplicationTests sleep 0.1 assert job.ran?, "Expected job to be run" - refute job.ran_in_different_thread?, "Expected job to run in the same thread" + assert job.ran_in_different_thread?, "Expected job to run in the same thread" end test "in test mode, explicitly draining the queue will process it in a separate thread" do @@ -160,12 +160,12 @@ module ApplicationTests test "a custom consumer implementation can be provided" do add_to_env_config "production", <<-RUBY require "my_queue_consumer" - config.queue = Rails::Queueing::Queue + config.queue = ActiveSupport::Queue config.queue_consumer = MyQueueConsumer RUBY app_file "lib/my_queue_consumer.rb", <<-RUBY - class MyQueueConsumer < Rails::Queueing::ThreadedConsumer + class MyQueueConsumer < ActiveSupport::ThreadedQueueConsumer attr_reader :started def start diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 52c07cee9f..03798d572a 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -100,8 +100,8 @@ module ApplicationTests `rails generate model book title:string` `bundle exec rake db:migrate` `bundle exec rake db:fixtures:load` - assert_match /#{expected[:database]}/, - ActiveRecord::Base.connection_config[:database] + assert_match(/#{expected[:database]}/, + ActiveRecord::Base.connection_config[:database]) require "#{app_path}/app/models/book" assert_equal 2, Book.count end @@ -129,8 +129,8 @@ module ApplicationTests assert_match(/CREATE TABLE \"books\"/, structure_dump) `bundle exec rake db:drop` `bundle exec rake db:structure:load` - assert_match /#{expected[:database]}/, - ActiveRecord::Base.connection_config[:database] + assert_match(/#{expected[:database]}/, + ActiveRecord::Base.connection_config[:database]) require "#{app_path}/app/models/book" #if structure is not loaded correctly, exception would be raised assert Book.count, 0 @@ -161,8 +161,8 @@ module ApplicationTests require "#{app_path}/app/models/book" #if structure is not loaded correctly, exception would be raised assert Book.count, 0 - assert_match /#{ActiveRecord::Base.configurations['test']['database']}/, - ActiveRecord::Base.connection_config[:database] + assert_match(/#{ActiveRecord::Base.configurations['test']['database']}/, + ActiveRecord::Base.connection_config[:database]) end end diff --git a/railties/test/queueing/container_test.rb b/railties/test/queueing/container_test.rb deleted file mode 100644 index 69e59a3871..0000000000 --- a/railties/test/queueing/container_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'abstract_unit' -require 'rails/queueing' - -module Rails - module Queueing - class ContainerTest < ActiveSupport::TestCase - def test_delegates_to_default - q = Queue.new - container = Container.new q - job = Object.new - - container.push job - assert_equal job, q.pop - end - - def test_access_default - q = Queue.new - container = Container.new q - assert_equal q, container[:default] - end - - def test_assign_queue - container = Container.new Object.new - q = Object.new - container[:foo] = q - assert_equal q, container[:foo] - end - end - end -end |