sidekiq 4.2.10 → 5.0.5
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -10
- data/5.0-Upgrade.md +56 -0
- data/COMM-LICENSE +1 -1
- data/Changes.md +60 -0
- data/Ent-Changes.md +29 -2
- data/Gemfile +3 -0
- data/Pro-Changes.md +28 -3
- data/README.md +3 -3
- data/bin/sidekiqctl +1 -1
- data/bin/sidekiqload +3 -8
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
- data/lib/sidekiq.rb +6 -15
- data/lib/sidekiq/api.rb +77 -31
- data/lib/sidekiq/cli.rb +15 -6
- data/lib/sidekiq/client.rb +20 -13
- data/lib/sidekiq/core_ext.rb +1 -119
- data/lib/sidekiq/delay.rb +41 -0
- data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
- data/lib/sidekiq/job_logger.rb +24 -0
- data/lib/sidekiq/job_retry.rb +228 -0
- data/lib/sidekiq/launcher.rb +1 -7
- data/lib/sidekiq/logging.rb +12 -0
- data/lib/sidekiq/middleware/server/active_record.rb +9 -0
- data/lib/sidekiq/processor.rb +63 -33
- data/lib/sidekiq/rails.rb +1 -73
- data/lib/sidekiq/redis_connection.rb +14 -3
- data/lib/sidekiq/testing.rb +12 -3
- data/lib/sidekiq/util.rb +1 -2
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +0 -4
- data/lib/sidekiq/web/application.rb +8 -13
- data/lib/sidekiq/web/helpers.rb +54 -16
- data/lib/sidekiq/worker.rb +99 -16
- data/sidekiq.gemspec +3 -7
- data/web/assets/javascripts/dashboard.js +17 -12
- data/web/assets/stylesheets/application-rtl.css +246 -0
- data/web/assets/stylesheets/application.css +336 -4
- data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
- data/web/locales/ar.yml +80 -0
- data/web/locales/fa.yml +1 -0
- data/web/locales/he.yml +79 -0
- data/web/locales/ur.yml +80 -0
- data/web/views/_footer.erb +2 -2
- data/web/views/_nav.erb +1 -1
- data/web/views/_paging.erb +1 -1
- data/web/views/busy.erb +9 -5
- data/web/views/dashboard.erb +1 -1
- data/web/views/layout.erb +10 -1
- data/web/views/morgue.erb +4 -4
- data/web/views/queue.erb +7 -7
- data/web/views/retries.erb +5 -5
- data/web/views/scheduled.erb +2 -2
- metadata +20 -83
- data/lib/sidekiq/middleware/server/logging.rb +0 -31
- data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
data/lib/sidekiq/launcher.rb
CHANGED
@@ -61,8 +61,6 @@ module Sidekiq
|
|
61
61
|
|
62
62
|
private unless $TESTING
|
63
63
|
|
64
|
-
JVM_RESERVED_SIGNALS = ['USR1', 'USR2'] # Don't Process#kill if we get these signals via the API
|
65
|
-
|
66
64
|
def heartbeat
|
67
65
|
results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
|
68
66
|
results.compact!
|
@@ -110,11 +108,7 @@ module Sidekiq
|
|
110
108
|
|
111
109
|
return unless msg
|
112
110
|
|
113
|
-
|
114
|
-
Sidekiq::CLI.instance.handle_signal(msg)
|
115
|
-
else
|
116
|
-
::Process.kill(msg, $$)
|
117
|
-
end
|
111
|
+
::Process.kill(msg, $$)
|
118
112
|
rescue => e
|
119
113
|
# ignore all redis/network issues
|
120
114
|
logger.error("heartbeat: #{e.message}")
|
data/lib/sidekiq/logging.rb
CHANGED
@@ -26,6 +26,18 @@ module Sidekiq
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
def self.job_hash_context(job_hash)
|
30
|
+
# If we're using a wrapper class, like ActiveJob, use the "wrapped"
|
31
|
+
# attribute to expose the underlying thing.
|
32
|
+
klass = job_hash['wrapped'.freeze] || job_hash["class".freeze]
|
33
|
+
bid = job_hash['bid'.freeze]
|
34
|
+
"#{klass} JID-#{job_hash['jid'.freeze]}#{" BID-#{bid}" if bid}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.with_job_hash_context(job_hash, &block)
|
38
|
+
with_context(job_hash_context(job_hash), &block)
|
39
|
+
end
|
40
|
+
|
29
41
|
def self.with_context(msg)
|
30
42
|
Thread.current[:sidekiq_context] ||= []
|
31
43
|
Thread.current[:sidekiq_context] << msg
|
@@ -2,6 +2,15 @@ module Sidekiq
|
|
2
2
|
module Middleware
|
3
3
|
module Server
|
4
4
|
class ActiveRecord
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
# With Rails 5+ we must use the Reloader **always**.
|
8
|
+
# The reloader handles code loading and db connection management.
|
9
|
+
if ::Rails::VERSION::MAJOR >= 5
|
10
|
+
raise ArgumentError, "Rails 5 no longer needs or uses the ActiveRecord middleware."
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
5
14
|
def call(*args)
|
6
15
|
yield
|
7
16
|
ensure
|
data/lib/sidekiq/processor.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'sidekiq/util'
|
3
3
|
require 'sidekiq/fetch'
|
4
|
+
require 'sidekiq/job_logger'
|
5
|
+
require 'sidekiq/job_retry'
|
4
6
|
require 'thread'
|
5
7
|
require 'concurrent/map'
|
6
8
|
require 'concurrent/atomic/atomic_fixnum'
|
@@ -37,7 +39,8 @@ module Sidekiq
|
|
37
39
|
@thread = nil
|
38
40
|
@strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
|
39
41
|
@reloader = Sidekiq.options[:reloader]
|
40
|
-
@
|
42
|
+
@logging = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
|
43
|
+
@retrier = Sidekiq::JobRetry.new
|
41
44
|
end
|
42
45
|
|
43
46
|
def terminate(wait=false)
|
@@ -116,32 +119,56 @@ module Sidekiq
|
|
116
119
|
nil
|
117
120
|
end
|
118
121
|
|
122
|
+
def dispatch(job_hash, queue)
|
123
|
+
# since middleware can mutate the job hash
|
124
|
+
# we clone here so we report the original
|
125
|
+
# job structure to the Web UI
|
126
|
+
pristine = cloned(job_hash)
|
127
|
+
|
128
|
+
Sidekiq::Logging.with_job_hash_context(job_hash) do
|
129
|
+
@retrier.global(pristine, queue) do
|
130
|
+
@logging.call(job_hash, queue) do
|
131
|
+
stats(pristine, queue) do
|
132
|
+
# Rails 5 requires a Reloader to wrap code execution. In order to
|
133
|
+
# constantize the worker and instantiate an instance, we have to call
|
134
|
+
# the Reloader. It handles code loading, db connection management, etc.
|
135
|
+
# Effectively this block denotes a "unit of work" to Rails.
|
136
|
+
@reloader.call do
|
137
|
+
klass = constantize(job_hash['class'.freeze])
|
138
|
+
worker = klass.new
|
139
|
+
worker.jid = job_hash['jid'.freeze]
|
140
|
+
@retrier.local(worker, pristine, queue) do
|
141
|
+
yield worker
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
119
150
|
def process(work)
|
120
151
|
jobstr = work.job
|
121
152
|
queue = work.queue_name
|
122
153
|
|
123
154
|
ack = false
|
124
155
|
begin
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
Sidekiq::Logging.with_context(log_context(job_hash)) do
|
133
|
-
ack = true
|
134
|
-
Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
|
135
|
-
@executor.call do
|
136
|
-
# Only ack if we either attempted to start this job or
|
137
|
-
# successfully completed it. This prevents us from
|
138
|
-
# losing jobs if a middleware raises an exception before yielding
|
139
|
-
execute_job(worker, cloned(job_hash['args'.freeze]))
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
156
|
+
# Treat malformed JSON as a special case: job goes straight to the morgue.
|
157
|
+
job_hash = nil
|
158
|
+
begin
|
159
|
+
job_hash = Sidekiq.load_json(jobstr)
|
160
|
+
rescue => ex
|
161
|
+
handle_exception(ex, { :context => "Invalid JSON for job", :jobstr => jobstr })
|
162
|
+
DeadSet.new.kill(jobstr)
|
144
163
|
ack = true
|
164
|
+
raise
|
165
|
+
end
|
166
|
+
|
167
|
+
ack = true
|
168
|
+
dispatch(job_hash, queue) do |worker|
|
169
|
+
Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
|
170
|
+
execute_job(worker, cloned(job_hash['args'.freeze]))
|
171
|
+
end
|
145
172
|
end
|
146
173
|
rescue Sidekiq::Shutdown
|
147
174
|
# Had to force kill this job because it didn't finish
|
@@ -149,20 +176,14 @@ module Sidekiq
|
|
149
176
|
# we didn't properly finish it.
|
150
177
|
ack = false
|
151
178
|
rescue Exception => ex
|
152
|
-
|
153
|
-
|
179
|
+
e = ex.is_a?(::Sidekiq::JobRetry::Skip) && ex.cause ? ex.cause : ex
|
180
|
+
handle_exception(e, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
|
181
|
+
raise e
|
154
182
|
ensure
|
155
183
|
work.acknowledge if ack
|
156
184
|
end
|
157
185
|
end
|
158
186
|
|
159
|
-
# If we're using a wrapper class, like ActiveJob, use the "wrapped"
|
160
|
-
# attribute to expose the underlying thing.
|
161
|
-
def log_context(item)
|
162
|
-
klass = item['wrapped'.freeze] || item['class'.freeze]
|
163
|
-
"#{klass} JID-#{item['jid'.freeze]}#{" BID-#{item['bid'.freeze]}" if item['bid'.freeze]}"
|
164
|
-
end
|
165
|
-
|
166
187
|
def execute_job(worker, cloned_args)
|
167
188
|
worker.perform(*cloned_args)
|
168
189
|
end
|
@@ -175,9 +196,9 @@ module Sidekiq
|
|
175
196
|
PROCESSED = Concurrent::AtomicFixnum.new
|
176
197
|
FAILURE = Concurrent::AtomicFixnum.new
|
177
198
|
|
178
|
-
def stats(
|
199
|
+
def stats(job_hash, queue)
|
179
200
|
tid = thread_identity
|
180
|
-
WORKER_STATE[tid] = {:queue => queue, :payload =>
|
201
|
+
WORKER_STATE[tid] = {:queue => queue, :payload => job_hash, :run_at => Time.now.to_i }
|
181
202
|
|
182
203
|
begin
|
183
204
|
yield
|
@@ -193,8 +214,17 @@ module Sidekiq
|
|
193
214
|
# Deep clone the arguments passed to the worker so that if
|
194
215
|
# the job fails, what is pushed back onto Redis hasn't
|
195
216
|
# been mutated by the worker.
|
196
|
-
def cloned(
|
197
|
-
Marshal.load(Marshal.dump(
|
217
|
+
def cloned(thing)
|
218
|
+
Marshal.load(Marshal.dump(thing))
|
219
|
+
end
|
220
|
+
|
221
|
+
def constantize(str)
|
222
|
+
names = str.split('::')
|
223
|
+
names.shift if names.empty? || names.first.empty?
|
224
|
+
|
225
|
+
names.inject(Object) do |constant, name|
|
226
|
+
constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
227
|
+
end
|
198
228
|
end
|
199
229
|
|
200
230
|
end
|
data/lib/sidekiq/rails.rb
CHANGED
@@ -1,36 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Sidekiq
|
3
|
-
def self.hook_rails!
|
4
|
-
return if defined?(@delay_removed)
|
5
|
-
|
6
|
-
ActiveSupport.on_load(:active_record) do
|
7
|
-
include Sidekiq::Extensions::ActiveRecord
|
8
|
-
end
|
9
|
-
|
10
|
-
ActiveSupport.on_load(:action_mailer) do
|
11
|
-
extend Sidekiq::Extensions::ActionMailer
|
12
|
-
end
|
13
|
-
|
14
|
-
Module.__send__(:include, Sidekiq::Extensions::Klass)
|
15
|
-
end
|
16
|
-
|
17
|
-
# Removes the generic aliases which MAY clash with names of already
|
18
|
-
# created methods by other applications. The methods `sidekiq_delay`,
|
19
|
-
# `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
|
20
|
-
def self.remove_delay!
|
21
|
-
@delay_removed = true
|
22
|
-
|
23
|
-
[Extensions::ActiveRecord,
|
24
|
-
Extensions::ActionMailer,
|
25
|
-
Extensions::Klass].each do |mod|
|
26
|
-
mod.module_eval do
|
27
|
-
remove_method :delay if respond_to?(:delay)
|
28
|
-
remove_method :delay_for if respond_to?(:delay_for)
|
29
|
-
remove_method :delay_until if respond_to?(:delay_until)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
3
|
class Rails < ::Rails::Engine
|
35
4
|
# We need to setup this up before any application configuration which might
|
36
5
|
# change Sidekiq middleware.
|
@@ -48,10 +17,6 @@ module Sidekiq
|
|
48
17
|
end
|
49
18
|
end
|
50
19
|
|
51
|
-
initializer 'sidekiq' do
|
52
|
-
Sidekiq.hook_rails!
|
53
|
-
end
|
54
|
-
|
55
20
|
config.after_initialize do
|
56
21
|
# This hook happens after all initializers are run, just before returning
|
57
22
|
# from config/environment.rb back to sidekiq/cli.rb.
|
@@ -62,38 +27,9 @@ module Sidekiq
|
|
62
27
|
#
|
63
28
|
Sidekiq.configure_server do |_|
|
64
29
|
if ::Rails::VERSION::MAJOR >= 5
|
65
|
-
|
66
|
-
# the ActiveRecord middleware so make sure it's not in the chain already.
|
67
|
-
if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
|
68
|
-
raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration."
|
69
|
-
elsif ::Rails.application.config.cache_classes
|
70
|
-
# The reloader API has proven to be troublesome under load in production.
|
71
|
-
# We won't use it at all when classes are cached, see #3154
|
72
|
-
Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
|
73
|
-
Sidekiq.options[:executor] = Sidekiq::Rails::Executor.new
|
74
|
-
else
|
75
|
-
Sidekiq.logger.debug { "Enabling Rails 5+ live code reloading, so hot!" }
|
76
|
-
Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
|
77
|
-
Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
class Executor
|
84
|
-
def initialize(app = ::Rails.application)
|
85
|
-
@app = app
|
86
|
-
end
|
87
|
-
|
88
|
-
def call
|
89
|
-
@app.executor.wrap do
|
90
|
-
yield
|
30
|
+
Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
|
91
31
|
end
|
92
32
|
end
|
93
|
-
|
94
|
-
def inspect
|
95
|
-
"#<Sidekiq::Rails::Executor @app=#{@app.class.name}>"
|
96
|
-
end
|
97
33
|
end
|
98
34
|
|
99
35
|
class Reloader
|
@@ -111,13 +47,5 @@ module Sidekiq
|
|
111
47
|
"#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
|
112
48
|
end
|
113
49
|
end
|
114
|
-
|
115
|
-
module PsychAutoload
|
116
|
-
def resolve_class(klass_name)
|
117
|
-
klass_name && klass_name.constantize
|
118
|
-
rescue NameError
|
119
|
-
super
|
120
|
-
end
|
121
|
-
end
|
122
50
|
end if defined?(::Rails)
|
123
51
|
end
|
@@ -8,8 +8,11 @@ module Sidekiq
|
|
8
8
|
class << self
|
9
9
|
|
10
10
|
def create(options={})
|
11
|
-
options
|
11
|
+
options.keys.each do |key|
|
12
|
+
options[key.to_sym] = options.delete(key)
|
13
|
+
end
|
12
14
|
|
15
|
+
options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
|
13
16
|
options[:url] ||= determine_redis_provider
|
14
17
|
|
15
18
|
size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)
|
@@ -67,7 +70,7 @@ module Sidekiq
|
|
67
70
|
opts.delete(:network_timeout)
|
68
71
|
end
|
69
72
|
|
70
|
-
opts[:driver] ||= 'ruby'
|
73
|
+
opts[:driver] ||= 'ruby'.freeze
|
71
74
|
|
72
75
|
# Issue #3303, redis-rb will silently retry an operation.
|
73
76
|
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
@@ -98,7 +101,15 @@ module Sidekiq
|
|
98
101
|
end
|
99
102
|
|
100
103
|
def determine_redis_provider
|
101
|
-
|
104
|
+
# If you have this in your environment:
|
105
|
+
# MY_REDIS_URL=redis://hostname.example.com:1238/4
|
106
|
+
# then set:
|
107
|
+
# REDIS_PROVIDER=MY_REDIS_URL
|
108
|
+
# and Sidekiq will find your custom URL variable with no custom
|
109
|
+
# initialization code at all.
|
110
|
+
ENV[
|
111
|
+
ENV['REDIS_PROVIDER'] || 'REDIS_URL'
|
112
|
+
]
|
102
113
|
end
|
103
114
|
|
104
115
|
end
|
data/lib/sidekiq/testing.rb
CHANGED
@@ -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
|
|
@@ -76,7 +85,7 @@ module Sidekiq
|
|
76
85
|
true
|
77
86
|
elsif Sidekiq::Testing.inline?
|
78
87
|
payloads.each do |job|
|
79
|
-
klass = job['class']
|
88
|
+
klass = Sidekiq::Testing.constantize(job['class'])
|
80
89
|
job['id'] ||= SecureRandom.hex(12)
|
81
90
|
job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
|
82
91
|
klass.process_job(job_hash)
|
@@ -309,7 +318,7 @@ module Sidekiq
|
|
309
318
|
worker_classes = jobs.map { |job| job["class"] }.uniq
|
310
319
|
|
311
320
|
worker_classes.each do |worker_class|
|
312
|
-
|
321
|
+
Sidekiq::Testing.constantize(worker_class).drain
|
313
322
|
end
|
314
323
|
end
|
315
324
|
end
|
@@ -317,7 +326,7 @@ module Sidekiq
|
|
317
326
|
end
|
318
327
|
end
|
319
328
|
|
320
|
-
if defined?(::Rails) && !Rails.env.test?
|
329
|
+
if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
|
321
330
|
puts("**************************************************")
|
322
331
|
puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
|
323
332
|
puts("**************************************************")
|
data/lib/sidekiq/util.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
require 'socket'
|
3
3
|
require 'securerandom'
|
4
4
|
require 'sidekiq/exception_handler'
|
5
|
-
require 'sidekiq/core_ext'
|
6
5
|
|
7
6
|
module Sidekiq
|
8
7
|
##
|
@@ -22,7 +21,7 @@ module Sidekiq
|
|
22
21
|
|
23
22
|
def safe_thread(name, &block)
|
24
23
|
Thread.new do
|
25
|
-
Thread.current['sidekiq_label'] = name
|
24
|
+
Thread.current['sidekiq_label'.freeze] = name
|
26
25
|
watchdog(name, &block)
|
27
26
|
end
|
28
27
|
end
|
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
@@ -234,7 +234,6 @@ module Sidekiq
|
|
234
234
|
get '/stats' do
|
235
235
|
sidekiq_stats = Sidekiq::Stats.new
|
236
236
|
redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
237
|
-
|
238
237
|
json(
|
239
238
|
sidekiq: {
|
240
239
|
processed: sidekiq_stats.processed,
|
@@ -247,7 +246,8 @@ module Sidekiq
|
|
247
246
|
dead: sidekiq_stats.dead_size,
|
248
247
|
default_latency: sidekiq_stats.default_queue_latency
|
249
248
|
},
|
250
|
-
redis: redis_stats
|
249
|
+
redis: redis_stats,
|
250
|
+
server_utc_time: server_utc_time
|
251
251
|
)
|
252
252
|
end
|
253
253
|
|
@@ -274,19 +274,14 @@ module Sidekiq
|
|
274
274
|
resp = case resp
|
275
275
|
when Array
|
276
276
|
resp
|
277
|
-
when Integer
|
278
|
-
[resp, {}, []]
|
279
277
|
else
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
else
|
286
|
-
{ "Content-Type" => "text/html", "Cache-Control" => "no-cache" }
|
287
|
-
end
|
278
|
+
headers = {
|
279
|
+
"Content-Type" => "text/html",
|
280
|
+
"Cache-Control" => "no-cache",
|
281
|
+
"Content-Language" => action.locale,
|
282
|
+
}
|
288
283
|
|
289
|
-
[200,
|
284
|
+
[200, headers, [resp]]
|
290
285
|
end
|
291
286
|
|
292
287
|
resp[1] = resp[1].dup
|