diff options
Diffstat (limited to 'railties')
24 files changed, 475 insertions, 15 deletions
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index 945063e55c..59c3c56e59 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -22,6 +22,7 @@ end module Rails autoload :Info, 'rails/info' autoload :InfoController, 'rails/info_controller' + autoload :Queueing, 'rails/queueing' class << self def application @@ -37,6 +38,25 @@ module Rails application.config end + # Rails.queue is the application's queue. You can push a job onto + # the queue by: + # + # Rails.queue.push job + # + # A job is an object that responds to +run+. Queue consumers will + # pop jobs off of the queue and invoke the queue's +run+ method. + # + # Note that depending on your queue implementation, jobs may not + # be executed in the same process as they were created in, and + # are never executed in the same thread as they were created in. + # + # If necessary, a queue implementation may need to serialize your + # job for distribution to another process. The documentation of + # your queue will specify the requirements for that serialization. + def queue + application.queue + end + def initialize! application.initialize! end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index b063e7681d..1680a6ed00 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -66,7 +66,7 @@ module Rails end end - attr_accessor :assets, :sandbox + attr_accessor :assets, :sandbox, :queue alias_method :sandbox?, :sandbox attr_reader :reloaders @@ -199,6 +199,14 @@ module Rails @config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd)) end + def queue #:nodoc: + @queue ||= build_queue + end + + def build_queue # :nodoc: + config.queue.new + end + def to_app self end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 37297f0496..a36dad0e98 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -11,7 +11,7 @@ module Rails :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, :preload_frameworks, :railties_order, :relative_url_root, :secret_token, :serve_static_assets, :ssl_options, :static_cache_control, :session_options, - :time_zone, :reload_classes_only_on_change, :use_schema_cache_dump + :time_zone, :reload_classes_only_on_change, :use_schema_cache_dump, :queue attr_writer :log_level attr_reader :encoding @@ -43,6 +43,7 @@ module Rails @autoflush_log = true @log_formatter = ActiveSupport::Logger::SimpleFormatter.new @use_schema_cache_dump = true + @queue = Queue @assets = ActiveSupport::OrderedOptions.new @assets.enabled = false diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 002c6026e4..6475b381f4 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -93,6 +93,13 @@ module Rails ActiveSupport::Dependencies.unhook! end end + + initializer :activate_queue_consumer do |app| + if config.queue == Queue + consumer = Rails::Queueing::ThreadedConsumer.start(app.queue) + at_exit { consumer.shutdown } + end + end end end end diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index 6f9a200aa9..7f9190b01a 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -3,12 +3,13 @@ require "active_support/core_ext/module/delegation" module Rails class Application class RoutesReloader - attr_reader :route_sets, :paths + attr_reader :route_sets, :paths, :external_routes delegate :execute_if_updated, :execute, :updated?, :to => :updater def initialize - @paths = [] - @route_sets = [] + @paths = [] + @route_sets = [] + @external_routes = [] end def reload! @@ -23,7 +24,12 @@ module Rails def updater @updater ||= begin - updater = ActiveSupport::FileUpdateChecker.new(paths) { reload! } + dirs = @external_routes.inject({}) do |hash, dir| + hash.merge(dir.to_s => ["rb"]) + end + + updater = ActiveSupport::FileUpdateChecker.new(paths, dirs) { reload! } + updater.execute updater end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 43ee396cbe..5d99b820c9 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -487,6 +487,7 @@ module Rails def routes @routes ||= ActionDispatch::Routing::RouteSet.new + @routes.draw_paths.concat paths["config/routes"].paths @routes.append(&Proc.new) if block_given? @routes end @@ -516,7 +517,7 @@ module Rails # # Blog::Engine.load_seed def load_seed - seed_file = paths["db/seeds"].existent.first + seed_file = paths["db/seeds.rb"].existent.first load(seed_file) if seed_file end @@ -544,11 +545,13 @@ module Rails end initializer :add_routing_paths do |app| - paths = self.paths["config/routes"].existent + paths = self.paths["config/routes.rb"].existent + external_paths = self.paths["config/routes"].paths if routes? || paths.any? app.routes_reloader.paths.unshift(*paths) app.routes_reloader.route_sets << routes + app.routes_reloader.external_routes.unshift(*external_paths) end end diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index d7405cb519..d3b42021fc 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -52,10 +52,11 @@ module Rails paths.add "config/environments", :glob => "#{Rails.env}.rb" paths.add "config/initializers", :glob => "**/*.rb" paths.add "config/locales", :glob => "*.{rb,yml}" - paths.add "config/routes", :with => "config/routes.rb" + paths.add "config/routes.rb" + paths.add "config/routes", :glob => "**/*.rb" paths.add "db" paths.add "db/migrate" - paths.add "db/seeds", :with => "db/seeds.rb" + paths.add "db/seeds.rb" paths.add "vendor", :load_path => true paths.add "vendor/assets", :glob => "*" paths diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index c457b5fbbc..e85d1b8fa2 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -40,7 +40,7 @@ module Rails def indent(content, multiplier = 2) spaces = " " * multiplier - content = content.each_line.map {|line| "#{spaces}#{line}" }.join + content = content.each_line.map {|line| line.blank? ? line : "#{spaces}#{line}" }.join end def wrap_with_namespace(content) diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index eb4dfa7c89..c486ae590e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -35,4 +35,7 @@ # Expands the lines which load the assets. config.assets.debug = true <%- end -%> + + # In development, use an in-memory queue for queueing + config.queue = Queue 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 1c980e5ce6..854e6e95cd 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 @@ -76,4 +76,8 @@ # Use default logging formatter so that PID and timestamp are not suppressed config.log_formatter = ::Logger::Formatter.new + + # Default the production mode queue to an in-memory queue. You will probably + # want to replace this with an out-of-process queueing solution + config.queue = Queue 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 b725dd19f6..b27b88a3c6 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 @@ -33,4 +33,7 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + + # Use the testing queue + config.queue = Rails::Queueing::TestQueue end diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb index 24026cf324..286e93c3cf 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb @@ -51,5 +51,4 @@ # root :to => 'welcome#index' # See how all your routes lay out with "rake routes" - -end +end
\ No newline at end of file diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 9b4a69f19f..b787d91821 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -1,3 +1,5 @@ +require "pathname" + module Rails module Paths # This object is an extended hash that behaves as root of the <tt>Rails::Paths</tt> system. @@ -114,7 +116,7 @@ module Rails class Path include Enumerable - attr_reader :path + attr_reader :path, :root attr_accessor :glob def initialize(root, current, paths, options = {}) @@ -180,6 +182,14 @@ module Rails @paths end + def paths + raise "You need to set a path root" unless @root.path + + map do |p| + Pathname.new(@root.path).join(p) + end + end + # Expands all paths against the root and return all unique values. def expanded raise "You need to set a path root" unless @root.path diff --git a/railties/lib/rails/queueing.rb b/railties/lib/rails/queueing.rb new file mode 100644 index 0000000000..b77940f821 --- /dev/null +++ b/railties/lib/rails/queueing.rb @@ -0,0 +1,65 @@ +module Rails + module Queueing + # 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 +contents+ + # 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 + attr_reader :contents + + def initialize + @contents = [] + end + + def drain + # run the jobs in a separate thread so assumptions of synchronous + # jobs are caught in test mode. + t = Thread.new do + while job = @contents.pop + job.run + end + end + t.join + end + + # implement the Queue API + def push(object) + @contents << object + 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 + job.run + end + end + self + end + + def shutdown + @queue.push nil + @thread.join + end + end + end +end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index ac5ac2b93e..252dd0e31a 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -41,6 +41,14 @@ module ApplicationTests FileUtils.rm_rf(new_app) if File.directory?(new_app) end + test "multiple queue construction is possible" do + require 'rails' + require "#{app_path}/config/environment" + mail_queue = Rails.application.build_queue + image_processing_queue = Rails.application.build_queue + assert_not_equal mail_queue, image_processing_queue + end + test "Rails.groups returns available groups" do require "rails" diff --git a/railties/test/application/paths_test.rb b/railties/test/application/paths_test.rb index 4029984ce9..e0893f53be 100644 --- a/railties/test/application/paths_test.rb +++ b/railties/test/application/paths_test.rb @@ -50,6 +50,8 @@ module ApplicationTests assert_path @paths["config/locales"], "config/locales/en.yml" assert_path @paths["config/environment"], "config/environment.rb" assert_path @paths["config/environments"], "config/environments/development.rb" + assert_path @paths["config/routes.rb"], "config/routes.rb" + assert_path @paths["config/routes"], "config/routes" assert_equal root("app", "controllers"), @paths["app/controllers"].expanded.first end diff --git a/railties/test/application/queue_test.rb b/railties/test/application/queue_test.rb new file mode 100644 index 0000000000..667565b031 --- /dev/null +++ b/railties/test/application/queue_test.rb @@ -0,0 +1,114 @@ +require 'isolation/abstract_unit' +require 'rack/test' + +module ApplicationTests + class GeneratorsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + end + + def teardown + teardown_app + end + + def app_const + @app_const ||= Class.new(Rails::Application) + end + + test "the queue is a TestQueue in test mode" do + app("test") + assert_kind_of Rails::Queueing::TestQueue, Rails.application.queue + assert_kind_of Rails::Queueing::TestQueue, Rails.queue + end + + test "the queue is a Queue in development mode" do + app("development") + assert_kind_of Queue, Rails.application.queue + assert_kind_of Queue, Rails.queue + end + + test "in development mode, an enqueued job will be processed in a separate thread" do + app("development") + current = Thread.current + + job = Struct.new(:origin, :target).new(Thread.current) + def job.run + self.target = Thread.current + end + + Rails.queue.push job + sleep 0.1 + + assert job.target, "The job was run" + assert_not_equal job.origin, job.target + end + + test "in test mode, explicitly draining the queue will process it in a separate thread" do + app("test") + current = Thread.current + + job = Struct.new(:origin, :target).new(Thread.current) + def job.run + self.target = Thread.current + end + + Rails.queue.push job + Rails.queue.drain + + assert job.target, "The job was run" + assert_not_equal job.origin, job.target + end + + test "in test mode, the queue can be observed" do + app("test") + + job = Class.new(Struct.new(:id)) do + def run + end + end + + jobs = (1..10).map do |id| + job.new(id) + end + + jobs.each do |job| + Rails.queue.push job + end + + assert_equal jobs, Rails.queue.contents + end + + test "a custom queue implementation can be provided" do + add_to_env_config "production", <<-RUBY + require "my_queue" + config.queue = MyQueue + RUBY + + app_file "lib/my_queue.rb", <<-RUBY + class MyQueue + def push(job) + job.run + end + end + RUBY + + app("production") + + assert_kind_of MyQueue, Rails.queue + + job = Class.new(Struct.new(:id, :ran)) do + def run + self.ran = true + end + end + + job1 = job.new(1) + Rails.queue.push job1 + + assert_equal true, job1.ran + end + end +end diff --git a/railties/test/application/route_inspect_test.rb b/railties/test/application/route_inspect_test.rb index 59dfe2ff75..453ba8196c 100644 --- a/railties/test/application/route_inspect_test.rb +++ b/railties/test/application/route_inspect_test.rb @@ -13,6 +13,7 @@ module ApplicationTests app.config.assets = ActiveSupport::OrderedOptions.new app.config.assets.prefix = '/sprockets' Rails.stubs(:application).returns(app) + Rails.stubs(:env).returns("development") end def test_displaying_routes_for_engines diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index 204f43a442..4560dcec69 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -166,6 +166,91 @@ module ApplicationTests assert_equal 'WIN', last_response.body end + test "routes drawing from config/routes" do + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + draw :external + end + RUBY + + app_file 'config/routes/external.rb', <<-RUBY + get ':controller/:action' + RUBY + + controller :success, <<-RUBY + class SuccessController < ActionController::Base + def index + render :text => "success!" + end + end + RUBY + + app 'development' + get '/success/index' + assert_equal 'success!', last_response.body + end + + {"development" => "baz", "production" => "bar"}.each do |mode, expected| + test "reloads routes when external configuration is changed in #{mode}" do + controller :foo, <<-RUBY + class FooController < ApplicationController + def bar + render :text => "bar" + end + + def baz + render :text => "baz" + end + end + RUBY + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + draw :external + end + RUBY + + app_file 'config/routes/external.rb', <<-RUBY + get 'foo', :to => 'foo#bar' + RUBY + + app(mode) + + get '/foo' + assert_equal 'bar', last_response.body + + app_file 'config/routes/external.rb', <<-RUBY + get 'foo', :to => 'foo#baz' + RUBY + + sleep 0.1 + + get '/foo' + assert_equal expected, last_response.body + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + draw :external + draw :other_external + end + RUBY + + app_file 'config/routes/other_external.rb', <<-RUBY + get 'win', :to => 'foo#baz' + RUBY + + sleep 0.1 + + get '/win' + + if mode == "development" + assert_equal expected, last_response.body + else + assert_equal 404, last_response.status + end + end + end + {"development" => "baz", "production" => "bar"}.each do |mode, expected| test "reloads routes when configuration is changed in #{mode}" do controller :foo, <<-RUBY diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index 76c34d4c50..6f00fc5d26 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -61,6 +61,15 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase run_generator ["account"] assert_file "app/views/test_app/account" end + + def test_namespaced_controller_dont_indent_blank_lines + run_generator + assert_file "app/controllers/test_app/account_controller.rb" do |content| + content.split("\n").each do |line| + assert_no_match line, /^\s+$/, "Don't indent blank lines" + end + end + end end class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index b28cc6e04d..7957186ba2 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -8,7 +8,7 @@ # Rails booted up. require 'fileutils' -require 'rubygems' +require 'bundler/setup' require 'minitest/autorun' require 'active_support/test_case' diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index d334034e7d..aa04cad033 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -29,6 +29,7 @@ class PathsTest < ActiveSupport::TestCase test "creating a root level path" do @root.add "app" assert_equal ["/foo/bar/app"], @root["app"].to_a + assert_equal [Pathname.new("/foo/bar/app")], @root["app"].paths end test "creating a root level path with options" do @@ -191,6 +192,7 @@ class PathsTest < ActiveSupport::TestCase @root["app"] = "/app" @root["app"].glob = "*.rb" assert_equal "*.rb", @root["app"].glob + assert_equal [Pathname.new("/app")], @root["app"].paths end test "it should be possible to override a path's default glob without assignment" do diff --git a/railties/test/queueing/test_queue_test.rb b/railties/test/queueing/test_queue_test.rb new file mode 100644 index 0000000000..de30e8cffd --- /dev/null +++ b/railties/test/queueing/test_queue_test.rb @@ -0,0 +1,44 @@ +require 'abstract_unit' +require 'rails/queueing' + +class TestQueueTest < ActiveSupport::TestCase + class Job + attr_reader :id + def initialize(id, &block) + @id = id + @block = block + end + + def run + @block.call if @block + end + end + + def setup + @queue = Rails::Queueing::TestQueue.new + end + + def test_contents + assert_equal [], @queue.contents + job = Job.new(1) + @queue.push job + assert_equal [job], @queue.contents + end + + def test_drain + t = nil + ran = false + + job = Job.new(1) do + ran = true + t = Thread.current + end + + @queue.push job + @queue.drain + + assert_equal [], @queue.contents + assert ran, "The job runs synchronously when the queue is drained" + assert_not_equal t, Thread.current + end +end diff --git a/railties/test/queueing/threaded_consumer_test.rb b/railties/test/queueing/threaded_consumer_test.rb new file mode 100644 index 0000000000..d00a67d511 --- /dev/null +++ b/railties/test/queueing/threaded_consumer_test.rb @@ -0,0 +1,65 @@ +require 'abstract_unit' +require 'rails/queueing' + +class TestThreadConsumer < ActiveSupport::TestCase + class Job + attr_reader :id + def initialize(id, &block) + @id = id + @block = block + end + + def run + @block.call if @block + end + end + + def setup + @queue = Queue.new + @consumer = Rails::Queueing::ThreadedConsumer.start(@queue) + end + + def teardown + @queue.push nil + end + + test "the jobs are executed" do + ran = false + + job = Job.new(1) do + ran = true + end + + @queue.push job + sleep 0.1 + assert_equal true, ran + end + + test "the jobs are not executed synchronously" do + ran = false + + job = Job.new(1) do + sleep 0.1 + ran = true + end + + @queue.push job + assert_equal false, ran + end + + test "shutting down the queue synchronously drains the jobs" do + ran = false + + job = Job.new(1) do + sleep 0.1 + ran = true + end + + @queue.push job + assert_equal false, ran + + @consumer.shutdown + + assert_equal true, ran + end +end |