sidekiq 4.2.10 → 6.1.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (106) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  3. data/.github/workflows/ci.yml +41 -0
  4. data/.gitignore +2 -1
  5. data/.standard.yml +20 -0
  6. data/5.0-Upgrade.md +56 -0
  7. data/6.0-Upgrade.md +72 -0
  8. data/COMM-LICENSE +12 -10
  9. data/Changes.md +354 -1
  10. data/Ent-2.0-Upgrade.md +37 -0
  11. data/Ent-Changes.md +111 -3
  12. data/Gemfile +16 -21
  13. data/Gemfile.lock +192 -0
  14. data/LICENSE +1 -1
  15. data/Pro-4.0-Upgrade.md +35 -0
  16. data/Pro-5.0-Upgrade.md +25 -0
  17. data/Pro-Changes.md +181 -4
  18. data/README.md +19 -33
  19. data/Rakefile +6 -8
  20. data/bin/sidekiq +26 -2
  21. data/bin/sidekiqload +37 -34
  22. data/bin/sidekiqmon +8 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  25. data/lib/generators/sidekiq/worker_generator.rb +21 -13
  26. data/lib/sidekiq.rb +86 -61
  27. data/lib/sidekiq/api.rb +320 -209
  28. data/lib/sidekiq/cli.rb +207 -217
  29. data/lib/sidekiq/client.rb +78 -51
  30. data/lib/sidekiq/delay.rb +41 -0
  31. data/lib/sidekiq/exception_handler.rb +12 -16
  32. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  33. data/lib/sidekiq/extensions/active_record.rb +13 -10
  34. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  35. data/lib/sidekiq/extensions/generic_proxy.rb +10 -4
  36. data/lib/sidekiq/fetch.rb +29 -30
  37. data/lib/sidekiq/job_logger.rb +63 -0
  38. data/lib/sidekiq/job_retry.rb +262 -0
  39. data/lib/sidekiq/launcher.rb +102 -69
  40. data/lib/sidekiq/logger.rb +165 -0
  41. data/lib/sidekiq/manager.rb +16 -19
  42. data/lib/sidekiq/middleware/chain.rb +15 -5
  43. data/lib/sidekiq/middleware/i18n.rb +5 -7
  44. data/lib/sidekiq/monitor.rb +133 -0
  45. data/lib/sidekiq/paginator.rb +18 -14
  46. data/lib/sidekiq/processor.rb +161 -82
  47. data/lib/sidekiq/rails.rb +27 -100
  48. data/lib/sidekiq/redis_connection.rb +60 -20
  49. data/lib/sidekiq/scheduled.rb +61 -35
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +24 -0
  52. data/lib/sidekiq/testing.rb +48 -28
  53. data/lib/sidekiq/testing/inline.rb +2 -1
  54. data/lib/sidekiq/util.rb +20 -16
  55. data/lib/sidekiq/version.rb +2 -1
  56. data/lib/sidekiq/web.rb +57 -57
  57. data/lib/sidekiq/web/action.rb +14 -14
  58. data/lib/sidekiq/web/application.rb +103 -84
  59. data/lib/sidekiq/web/csrf_protection.rb +158 -0
  60. data/lib/sidekiq/web/helpers.rb +126 -71
  61. data/lib/sidekiq/web/router.rb +18 -17
  62. data/lib/sidekiq/worker.rb +164 -41
  63. data/sidekiq.gemspec +15 -27
  64. data/web/assets/javascripts/application.js +25 -27
  65. data/web/assets/javascripts/dashboard.js +33 -37
  66. data/web/assets/stylesheets/application-dark.css +143 -0
  67. data/web/assets/stylesheets/application-rtl.css +246 -0
  68. data/web/assets/stylesheets/application.css +385 -10
  69. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  70. data/web/assets/stylesheets/bootstrap.css +2 -2
  71. data/web/locales/ar.yml +81 -0
  72. data/web/locales/de.yml +14 -2
  73. data/web/locales/en.yml +4 -0
  74. data/web/locales/es.yml +4 -3
  75. data/web/locales/fa.yml +1 -0
  76. data/web/locales/fr.yml +2 -2
  77. data/web/locales/he.yml +79 -0
  78. data/web/locales/ja.yml +9 -4
  79. data/web/locales/lt.yml +83 -0
  80. data/web/locales/pl.yml +4 -4
  81. data/web/locales/ru.yml +4 -0
  82. data/web/locales/ur.yml +80 -0
  83. data/web/locales/vi.yml +83 -0
  84. data/web/views/_footer.erb +5 -2
  85. data/web/views/_job_info.erb +2 -1
  86. data/web/views/_nav.erb +4 -18
  87. data/web/views/_paging.erb +1 -1
  88. data/web/views/busy.erb +15 -8
  89. data/web/views/dashboard.erb +1 -1
  90. data/web/views/dead.erb +2 -2
  91. data/web/views/layout.erb +12 -2
  92. data/web/views/morgue.erb +9 -6
  93. data/web/views/queue.erb +18 -8
  94. data/web/views/queues.erb +11 -1
  95. data/web/views/retries.erb +14 -7
  96. data/web/views/retry.erb +2 -2
  97. data/web/views/scheduled.erb +7 -4
  98. metadata +41 -188
  99. data/.github/issue_template.md +0 -9
  100. data/.travis.yml +0 -18
  101. data/bin/sidekiqctl +0 -99
  102. data/lib/sidekiq/core_ext.rb +0 -119
  103. data/lib/sidekiq/logging.rb +0 -106
  104. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  105. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  106. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
@@ -0,0 +1,24 @@
1
+ #
2
+ # Sidekiq's systemd integration allows Sidekiq to inform systemd:
3
+ # 1. when it has successfully started
4
+ # 2. when it is starting shutdown
5
+ # 3. periodically for a liveness check with a watchdog thread
6
+ #
7
+ module Sidekiq
8
+ def self.start_watchdog
9
+ usec = Integer(ENV["WATCHDOG_USEC"])
10
+ return Sidekiq.logger.error("systemd Watchdog too fast: " + usec) if usec < 1_000_000
11
+
12
+ sec_f = usec / 1_000_000.0
13
+ # "It is recommended that a daemon sends a keep-alive notification message
14
+ # to the service manager every half of the time returned here."
15
+ ping_f = sec_f / 2
16
+ Sidekiq.logger.info "Pinging systemd watchdog every #{ping_f.round(1)} sec"
17
+ Thread.new do
18
+ loop do
19
+ sleep ping_f
20
+ Sidekiq::SdNotify.watchdog
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
- require 'securerandom'
3
- require 'sidekiq'
4
2
 
5
- module Sidekiq
3
+ require "securerandom"
4
+ require "sidekiq"
6
5
 
6
+ module Sidekiq
7
7
  class Testing
8
8
  class << self
9
9
  attr_accessor :__test_mode
10
10
 
11
11
  def __set_test_mode(mode)
12
12
  if block_given?
13
- current_mode = self.__test_mode
13
+ current_mode = __test_mode
14
14
  begin
15
15
  self.__test_mode = mode
16
16
  yield
@@ -35,19 +35,19 @@ module Sidekiq
35
35
  end
36
36
 
37
37
  def enabled?
38
- self.__test_mode != :disable
38
+ __test_mode != :disable
39
39
  end
40
40
 
41
41
  def disabled?
42
- self.__test_mode == :disable
42
+ __test_mode == :disable
43
43
  end
44
44
 
45
45
  def fake?
46
- self.__test_mode == :fake
46
+ __test_mode == :fake
47
47
  end
48
48
 
49
49
  def inline?
50
- self.__test_mode == :inline
50
+ __test_mode == :inline
51
51
  end
52
52
 
53
53
  def server_middleware
@@ -55,6 +55,15 @@ module Sidekiq
55
55
  yield @server_chain if block_given?
56
56
  @server_chain
57
57
  end
58
+
59
+ def constantize(str)
60
+ names = str.split("::")
61
+ names.shift if names.empty? || names.first.empty?
62
+
63
+ names.inject(Object) do |constant, name|
64
+ constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
65
+ end
66
+ end
58
67
  end
59
68
  end
60
69
 
@@ -63,31 +72,31 @@ module Sidekiq
63
72
 
64
73
  class EmptyQueueError < RuntimeError; end
65
74
 
66
- class Client
67
- alias_method :raw_push_real, :raw_push
68
-
75
+ module TestingClient
69
76
  def raw_push(payloads)
70
77
  if Sidekiq::Testing.fake?
71
78
  payloads.each do |job|
72
79
  job = Sidekiq.load_json(Sidekiq.dump_json(job))
73
- job.merge!('enqueued_at' => Time.now.to_f) unless job['at']
74
- Queues.push(job['queue'], job['class'], job)
80
+ job["enqueued_at"] = Time.now.to_f unless job["at"]
81
+ Queues.push(job["queue"], job["class"], job)
75
82
  end
76
83
  true
77
84
  elsif Sidekiq::Testing.inline?
78
85
  payloads.each do |job|
79
- klass = job['class'].constantize
80
- job['id'] ||= SecureRandom.hex(12)
86
+ klass = Sidekiq::Testing.constantize(job["class"])
87
+ job["id"] ||= SecureRandom.hex(12)
81
88
  job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
82
89
  klass.process_job(job_hash)
83
90
  end
84
91
  true
85
92
  else
86
- raw_push_real(payloads)
93
+ super
87
94
  end
88
95
  end
89
96
  end
90
97
 
98
+ Sidekiq::Client.prepend TestingClient
99
+
91
100
  module Queues
92
101
  ##
93
102
  # The Queues class is only for testing the fake queue implementation.
@@ -246,27 +255,26 @@ module Sidekiq
246
255
  # Then I should receive a welcome email to "foo@example.com"
247
256
  #
248
257
  module ClassMethods
249
-
250
258
  # Queue for this worker
251
259
  def queue
252
- self.sidekiq_options["queue"]
260
+ get_sidekiq_options["queue"]
253
261
  end
254
262
 
255
263
  # Jobs queued for this worker
256
264
  def jobs
257
- Queues.jobs_by_worker[self.to_s]
265
+ Queues.jobs_by_worker[to_s]
258
266
  end
259
267
 
260
268
  # Clear all jobs for this worker
261
269
  def clear
262
- Queues.clear_for(queue, self.to_s)
270
+ Queues.clear_for(queue, to_s)
263
271
  end
264
272
 
265
273
  # Drain and run all jobs for this worker
266
274
  def drain
267
275
  while jobs.any?
268
276
  next_job = jobs.first
269
- Queues.delete_for(next_job["jid"], next_job["queue"], self.to_s)
277
+ Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
270
278
  process_job(next_job)
271
279
  end
272
280
  end
@@ -275,16 +283,16 @@ module Sidekiq
275
283
  def perform_one
276
284
  raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
277
285
  next_job = jobs.first
278
- Queues.delete_for(next_job["jid"], queue, self.to_s)
286
+ Queues.delete_for(next_job["jid"], queue, to_s)
279
287
  process_job(next_job)
280
288
  end
281
289
 
282
290
  def process_job(job)
283
291
  worker = new
284
- worker.jid = job['jid']
285
- worker.bid = job['bid'] if worker.respond_to?(:bid=)
286
- Sidekiq::Testing.server_middleware.invoke(worker, job, job['queue']) do
287
- execute_job(worker, job['args'])
292
+ worker.jid = job["jid"]
293
+ worker.bid = job["bid"] if worker.respond_to?(:bid=)
294
+ Sidekiq::Testing.server_middleware.invoke(worker, job, job["queue"]) do
295
+ execute_job(worker, job["args"])
288
296
  end
289
297
  end
290
298
 
@@ -309,15 +317,27 @@ module Sidekiq
309
317
  worker_classes = jobs.map { |job| job["class"] }.uniq
310
318
 
311
319
  worker_classes.each do |worker_class|
312
- worker_class.constantize.drain
320
+ Sidekiq::Testing.constantize(worker_class).drain
313
321
  end
314
322
  end
315
323
  end
316
324
  end
317
325
  end
326
+
327
+ module TestingExtensions
328
+ def jobs_for(klass)
329
+ jobs.select do |job|
330
+ marshalled = job["args"][0]
331
+ marshalled.index(klass.to_s) && YAML.load(marshalled)[0] == klass
332
+ end
333
+ end
334
+ end
335
+
336
+ Sidekiq::Extensions::DelayedMailer.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedMailer)
337
+ Sidekiq::Extensions::DelayedModel.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedModel)
318
338
  end
319
339
 
320
- if defined?(::Rails) && !Rails.env.test?
340
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
321
341
  puts("**************************************************")
322
342
  puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
323
343
  puts("**************************************************")
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/testing'
2
+
3
+ require "sidekiq/testing"
3
4
 
4
5
  ##
5
6
  # The Sidekiq inline infrastructure overrides perform_async so that it
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
- require 'socket'
3
- require 'securerandom'
4
- require 'sidekiq/exception_handler'
5
- require 'sidekiq/core_ext'
2
+
3
+ require "socket"
4
+ require "securerandom"
5
+ require "sidekiq/exception_handler"
6
6
 
7
7
  module Sidekiq
8
8
  ##
@@ -11,18 +11,16 @@ module Sidekiq
11
11
  module Util
12
12
  include ExceptionHandler
13
13
 
14
- EXPIRY = 60 * 60 * 24
15
-
16
14
  def watchdog(last_words)
17
15
  yield
18
16
  rescue Exception => ex
19
- handle_exception(ex, { context: last_words })
17
+ handle_exception(ex, {context: last_words})
20
18
  raise ex
21
19
  end
22
20
 
23
21
  def safe_thread(name, &block)
24
22
  Thread.new do
25
- Thread.current['sidekiq_label'] = name
23
+ Thread.current.name = name
26
24
  watchdog(name, &block)
27
25
  end
28
26
  end
@@ -35,8 +33,12 @@ module Sidekiq
35
33
  Sidekiq.redis(&block)
36
34
  end
37
35
 
36
+ def tid
37
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
38
+ end
39
+
38
40
  def hostname
39
- ENV['DYNO'] || Socket.gethostname
41
+ ENV["DYNO"] || Socket.gethostname
40
42
  end
41
43
 
42
44
  def process_nonce
@@ -44,18 +46,20 @@ module Sidekiq
44
46
  end
45
47
 
46
48
  def identity
47
- @@identity ||= "#{hostname}:#{$$}:#{process_nonce}"
49
+ @@identity ||= "#{hostname}:#{::Process.pid}:#{process_nonce}"
48
50
  end
49
51
 
50
- def fire_event(event, reverse=false)
52
+ def fire_event(event, options = {})
53
+ reverse = options[:reverse]
54
+ reraise = options[:reraise]
55
+
51
56
  arr = Sidekiq.options[:lifecycle_events][event]
52
57
  arr.reverse! if reverse
53
58
  arr.each do |block|
54
- begin
55
- block.call
56
- rescue => ex
57
- handle_exception(ex, { context: "Exception during Sidekiq lifecycle event.", event: event })
58
- end
59
+ block.call
60
+ rescue => ex
61
+ handle_exception(ex, {context: "Exception during Sidekiq lifecycle event.", event: event})
62
+ raise ex if reraise
59
63
  end
60
64
  arr.clear
61
65
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Sidekiq
3
- VERSION = "4.2.10"
4
+ VERSION = "6.1.2"
4
5
  end
@@ -1,36 +1,38 @@
1
1
  # frozen_string_literal: true
2
- require 'erb'
3
2
 
4
- require 'sidekiq'
5
- require 'sidekiq/api'
6
- require 'sidekiq/paginator'
7
- require 'sidekiq/web/helpers'
3
+ require "erb"
8
4
 
9
- require 'sidekiq/web/router'
10
- require 'sidekiq/web/action'
11
- require 'sidekiq/web/application'
5
+ require "sidekiq"
6
+ require "sidekiq/api"
7
+ require "sidekiq/paginator"
8
+ require "sidekiq/web/helpers"
12
9
 
13
- require 'rack/protection'
10
+ require "sidekiq/web/router"
11
+ require "sidekiq/web/action"
12
+ require "sidekiq/web/application"
13
+ require "sidekiq/web/csrf_protection"
14
14
 
15
- require 'rack/builder'
16
- require 'rack/file'
17
- require 'rack/session/cookie'
15
+ require "rack/content_length"
16
+
17
+ require "rack/builder"
18
+ require "rack/file"
19
+ require "rack/session/cookie"
18
20
 
19
21
  module Sidekiq
20
22
  class Web
21
23
  ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../web")
22
- VIEWS = "#{ROOT}/views".freeze
23
- LOCALES = ["#{ROOT}/locales".freeze]
24
- LAYOUT = "#{VIEWS}/layout.erb".freeze
25
- ASSETS = "#{ROOT}/assets".freeze
24
+ VIEWS = "#{ROOT}/views"
25
+ LOCALES = ["#{ROOT}/locales"]
26
+ LAYOUT = "#{VIEWS}/layout.erb"
27
+ ASSETS = "#{ROOT}/assets"
26
28
 
27
29
  DEFAULT_TABS = {
28
- "Dashboard" => '',
29
- "Busy" => 'busy',
30
- "Queues" => 'queues',
31
- "Retries" => 'retries',
32
- "Scheduled" => 'scheduled',
33
- "Dead" => 'morgue',
30
+ "Dashboard" => "",
31
+ "Busy" => "busy",
32
+ "Queues" => "queues",
33
+ "Retries" => "retries",
34
+ "Scheduled" => "scheduled",
35
+ "Dead" => "morgue"
34
36
  }
35
37
 
36
38
  class << self
@@ -64,11 +66,11 @@ module Sidekiq
64
66
  end
65
67
 
66
68
  def enable(*opts)
67
- opts.each {|key| set(key, true) }
69
+ opts.each { |key| set(key, true) }
68
70
  end
69
71
 
70
72
  def disable(*opts)
71
- opts.each {|key| set(key, false) }
73
+ opts.each { |key| set(key, false) }
72
74
  end
73
75
 
74
76
  # Helper for the Sinatra syntax: Sidekiq::Web.set(:session_secret, Rails.application.secrets...)
@@ -81,10 +83,10 @@ module Sidekiq
81
83
  end
82
84
 
83
85
  def self.inherited(child)
84
- child.app_url = self.app_url
85
- child.session_secret = self.session_secret
86
- child.redis_pool = self.redis_pool
87
- child.sessions = self.sessions
86
+ child.app_url = app_url
87
+ child.session_secret = session_secret
88
+ child.redis_pool = redis_pool
89
+ child.sessions = sessions
88
90
  end
89
91
 
90
92
  def settings
@@ -113,11 +115,11 @@ module Sidekiq
113
115
  end
114
116
 
115
117
  def enable(*opts)
116
- opts.each {|key| set(key, true) }
118
+ opts.each { |key| set(key, true) }
117
119
  end
118
120
 
119
121
  def disable(*opts)
120
- opts.each {|key| set(key, false) }
122
+ opts.each { |key| set(key, false) }
121
123
  end
122
124
 
123
125
  def set(attribute, value)
@@ -145,32 +147,39 @@ module Sidekiq
145
147
  private
146
148
 
147
149
  def using?(middleware)
148
- middlewares.any? do |(m,_)|
149
- m.kind_of?(Array) && (m[0] == middleware || m[0].kind_of?(middleware))
150
+ middlewares.any? do |(m, _)|
151
+ m.is_a?(Array) && (m[0] == middleware || m[0].is_a?(middleware))
150
152
  end
151
153
  end
152
154
 
153
155
  def build_sessions
154
156
  middlewares = self.middlewares
155
157
 
156
- unless using?(::Rack::Protection) || ENV['RACK_ENV'] == 'test'
157
- middlewares.unshift [[::Rack::Protection, { use: :authenticity_token }], nil]
158
- end
159
-
160
158
  s = sessions
161
- return unless s
162
159
 
163
- unless using? ::Rack::Session::Cookie
164
- unless secret = Web.session_secret
165
- require 'securerandom'
160
+ # turn on CSRF protection if sessions are enabled and this is not the test env
161
+ if s && !using?(CsrfProtection) && ENV["RACK_ENV"] != "test"
162
+ middlewares.unshift [[CsrfProtection], nil]
163
+ end
164
+
165
+ if s && !using?(::Rack::Session::Cookie)
166
+ unless (secret = Web.session_secret)
167
+ require "securerandom"
166
168
  secret = SecureRandom.hex(64)
167
169
  end
168
170
 
169
- options = { secret: secret }
171
+ options = {secret: secret}
170
172
  options = options.merge(s.to_hash) if s.respond_to? :to_hash
171
173
 
172
174
  middlewares.unshift [[::Rack::Session::Cookie, options], nil]
173
175
  end
176
+
177
+ # Since Sidekiq::WebApplication no longer calculates its own
178
+ # Content-Length response header, we must ensure that the Rack middleware
179
+ # that does this is loaded
180
+ unless using? ::Rack::ContentLength
181
+ middlewares.unshift [[::Rack::ContentLength], nil]
182
+ end
174
183
  end
175
184
 
176
185
  def build
@@ -180,13 +189,13 @@ module Sidekiq
180
189
  klass = self.class
181
190
 
182
191
  ::Rack::Builder.new do
183
- %w(stylesheets javascripts images).each do |asset_dir|
192
+ %w[stylesheets javascripts images].each do |asset_dir|
184
193
  map "/#{asset_dir}" do
185
- run ::Rack::File.new("#{ASSETS}/#{asset_dir}", { 'Cache-Control' => 'public, max-age=86400' })
194
+ run ::Rack::File.new("#{ASSETS}/#{asset_dir}", {"Cache-Control" => "public, max-age=86400"})
186
195
  end
187
196
  end
188
197
 
189
- middlewares.each {|middleware, block| use(*middleware, &block) }
198
+ middlewares.each { |middleware, block| use(*middleware, &block) }
190
199
 
191
200
  run WebApplication.new(klass)
192
201
  end
@@ -196,18 +205,9 @@ module Sidekiq
196
205
  Sidekiq::WebApplication.helpers WebHelpers
197
206
  Sidekiq::WebApplication.helpers Sidekiq::Paginator
198
207
 
199
- Sidekiq::WebAction.class_eval "def _render\n#{ERB.new(File.read(Web::LAYOUT)).src}\nend"
200
- end
201
-
202
- if defined?(::ActionDispatch::Request::Session) &&
203
- !::ActionDispatch::Request::Session.method_defined?(:each)
204
- # mperham/sidekiq#2460
205
- # Rack apps can't reuse the Rails session store without
206
- # this monkeypatch, fixed in Rails 5.
207
- class ActionDispatch::Request::Session
208
- def each(&block)
209
- hash = self.to_hash
210
- hash.each(&block)
208
+ Sidekiq::WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
209
+ def _render
210
+ #{ERB.new(File.read(Web::LAYOUT)).src}
211
211
  end
212
- end
212
+ RUBY
213
213
  end