puma 3.7.1 → 4.1.0

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

Potentially problematic release.


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

Files changed (74) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +229 -1
  3. data/README.md +179 -212
  4. data/docs/architecture.md +37 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +24 -4
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +28 -0
  10. data/docs/restart.md +41 -0
  11. data/docs/signals.md +56 -3
  12. data/docs/systemd.md +130 -37
  13. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  14. data/ext/puma_http11/extconf.rb +8 -0
  15. data/ext/puma_http11/http11_parser.c +84 -84
  16. data/ext/puma_http11/http11_parser.rl +9 -9
  17. data/ext/puma_http11/mini_ssl.c +105 -9
  18. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  19. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +30 -6
  21. data/lib/puma.rb +10 -0
  22. data/lib/puma/accept_nonblock.rb +2 -0
  23. data/lib/puma/app/status.rb +13 -0
  24. data/lib/puma/binder.rb +33 -18
  25. data/lib/puma/cli.rb +48 -33
  26. data/lib/puma/client.rb +94 -22
  27. data/lib/puma/cluster.rb +69 -21
  28. data/lib/puma/commonlogger.rb +2 -0
  29. data/lib/puma/configuration.rb +134 -136
  30. data/lib/puma/const.rb +16 -2
  31. data/lib/puma/control_cli.rb +31 -18
  32. data/lib/puma/convenient.rb +5 -3
  33. data/lib/puma/daemon_ext.rb +2 -0
  34. data/lib/puma/delegation.rb +2 -0
  35. data/lib/puma/detect.rb +2 -0
  36. data/lib/puma/dsl.rb +349 -113
  37. data/lib/puma/events.rb +8 -4
  38. data/lib/puma/io_buffer.rb +3 -6
  39. data/lib/puma/jruby_restart.rb +2 -1
  40. data/lib/puma/launcher.rb +60 -36
  41. data/lib/puma/minissl.rb +85 -28
  42. data/lib/puma/null_io.rb +2 -0
  43. data/lib/puma/plugin.rb +2 -0
  44. data/lib/puma/plugin/tmp_restart.rb +3 -2
  45. data/lib/puma/rack/builder.rb +4 -1
  46. data/lib/puma/rack/urlmap.rb +2 -0
  47. data/lib/puma/rack_default.rb +2 -0
  48. data/lib/puma/reactor.rb +218 -30
  49. data/lib/puma/runner.rb +18 -4
  50. data/lib/puma/server.rb +149 -56
  51. data/lib/puma/single.rb +16 -5
  52. data/lib/puma/state_file.rb +2 -0
  53. data/lib/puma/tcp_logger.rb +2 -0
  54. data/lib/puma/thread_pool.rb +59 -6
  55. data/lib/puma/util.rb +2 -6
  56. data/lib/rack/handler/puma.rb +58 -19
  57. data/tools/jungle/README.md +12 -2
  58. data/tools/jungle/init.d/README.md +2 -0
  59. data/tools/jungle/init.d/puma +8 -8
  60. data/tools/jungle/init.d/run-puma +1 -1
  61. data/tools/jungle/rc.d/README.md +74 -0
  62. data/tools/jungle/rc.d/puma +61 -0
  63. data/tools/jungle/rc.d/puma.conf +10 -0
  64. data/tools/trickletest.rb +1 -1
  65. metadata +25 -85
  66. data/.github/issue_template.md +0 -20
  67. data/Gemfile +0 -12
  68. data/Manifest.txt +0 -77
  69. data/Rakefile +0 -158
  70. data/gemfiles/2.1-Gemfile +0 -12
  71. data/lib/puma/compat.rb +0 -14
  72. data/lib/puma/java_io_buffer.rb +0 -45
  73. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  74. data/puma.gemspec +0 -52
data/lib/puma/cluster.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/runner'
2
4
  require 'puma/util'
3
5
  require 'puma/plugin'
@@ -5,9 +7,18 @@ require 'puma/plugin'
5
7
  require 'time'
6
8
 
7
9
  module Puma
10
+ # This class is instantiated by the `Puma::Launcher` and used
11
+ # to boot and serve a Ruby application when puma "workers" are needed
12
+ # i.e. when using multi-processes. For example `$ puma -w 5`
13
+ #
14
+ # At the core of this class is running an instance of `Puma::Server` which
15
+ # gets created via the `start_server` method from the `Puma::Runner` class
16
+ # that this inherits from.
17
+ #
18
+ # An instance of this class will spawn the number of processes passed in
19
+ # via the `spawn_workers` method call. Each worker will have it's own
20
+ # instance of a `Puma::Server`.
8
21
  class Cluster < Runner
9
- WORKER_CHECK_INTERVAL = 5
10
-
11
22
  def initialize(cli, events)
12
23
  super cli, events
13
24
 
@@ -24,7 +35,35 @@ module Puma
24
35
  @workers.each { |x| x.term }
25
36
 
26
37
  begin
27
- Process.waitall
38
+ if RUBY_VERSION < '2.6'
39
+ @workers.each do |w|
40
+ begin
41
+ Process.waitpid(w.pid)
42
+ rescue Errno::ECHILD
43
+ # child is already terminated
44
+ end
45
+ end
46
+ else
47
+ # below code is for a bug in Ruby 2.6+, above waitpid call hangs
48
+ t_st = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+ pids = @workers.map(&:pid)
50
+ loop do
51
+ pids.reject! do |w_pid|
52
+ begin
53
+ if Process.waitpid(w_pid, Process::WNOHANG)
54
+ log " worker status: #{$?}"
55
+ true
56
+ end
57
+ rescue Errno::ECHILD
58
+ true # child is already terminated
59
+ end
60
+ end
61
+ break if pids.empty?
62
+ sleep 0.5
63
+ end
64
+ t_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
+ log format(" worker shutdown time: %6.2f", t_end - t_st)
66
+ end
28
67
  rescue Interrupt
29
68
  log "! Cancelled waiting for workers"
30
69
  end
@@ -56,12 +95,13 @@ module Puma
56
95
  @signal = "TERM"
57
96
  @options = options
58
97
  @first_term_sent = nil
98
+ @started_at = Time.now
59
99
  @last_checkin = Time.now
60
100
  @last_status = '{}'
61
101
  @dead = false
62
102
  end
63
103
 
64
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status
104
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
65
105
 
66
106
  def booted?
67
107
  @stage == :booted
@@ -170,7 +210,7 @@ module Puma
170
210
  def check_workers(force=false)
171
211
  return if !force && @next_check && @next_check >= Time.now
172
212
 
173
- @next_check = Time.now + WORKER_CHECK_INTERVAL
213
+ @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
174
214
 
175
215
  any = false
176
216
 
@@ -187,14 +227,11 @@ module Puma
187
227
  # during this loop by giving the kernel time to kill them.
188
228
  sleep 1 if any
189
229
 
190
- while @workers.any?
191
- pid = Process.waitpid(-1, Process::WNOHANG)
192
- break unless pid
193
-
194
- @workers.delete_if { |w| w.pid == pid }
230
+ pids = []
231
+ while pid = Process.waitpid(-1, Process::WNOHANG) do
232
+ pids << pid
195
233
  end
196
-
197
- @workers.delete_if(&:dead?)
234
+ @workers.reject! { |w| w.dead? || pids.include?(w.pid) }
198
235
 
199
236
  cull_workers
200
237
  spawn_workers
@@ -224,12 +261,13 @@ module Puma
224
261
  begin
225
262
  @wakeup.write "!" unless @wakeup.closed?
226
263
  rescue SystemCallError, IOError
264
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
227
265
  end
228
266
  end
229
267
 
230
268
  def worker(index, master)
231
- title = "puma: cluster worker #{index}: #{master}"
232
- title << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
269
+ title = "puma: cluster worker #{index}: #{master}"
270
+ title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
233
271
  $0 = title
234
272
 
235
273
  Signal.trap "SIGINT", "IGNORE"
@@ -267,6 +305,7 @@ module Puma
267
305
  begin
268
306
  @worker_write << "b#{Process.pid}\n"
269
307
  rescue SystemCallError, IOError
308
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
270
309
  STDERR.puts "Master seems to have exited, exiting."
271
310
  return
272
311
  end
@@ -275,13 +314,16 @@ module Puma
275
314
  base_payload = "p#{Process.pid}"
276
315
 
277
316
  while true
278
- sleep WORKER_CHECK_INTERVAL
317
+ sleep Const::WORKER_CHECK_INTERVAL
279
318
  begin
280
- b = server.backlog
281
- r = server.running
282
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r} }\n!
319
+ b = server.backlog || 0
320
+ r = server.running || 0
321
+ t = server.pool_capacity || 0
322
+ m = server.max_threads || 0
323
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m} }\n!
283
324
  io << payload
284
325
  rescue IOError
326
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
285
327
  break
286
328
  end
287
329
  end
@@ -337,8 +379,8 @@ module Puma
337
379
  def stats
338
380
  old_worker_count = @workers.count { |w| w.phase != @phase }
339
381
  booted_worker_count = @workers.count { |w| w.booted? }
340
- worker_status = '[' + @workers.map{ |w| %Q!{ "pid": #{w.pid}, "index": #{w.index}, "phase": #{w.phase}, "booted": #{w.booted?}, "last_checkin": "#{w.last_checkin.utc.iso8601}", "last_status": #{w.last_status} }!}.join(",") + ']'
341
- %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
382
+ worker_status = '[' + @workers.map { |w| %Q!{ "started_at": "#{w.started_at.utc.iso8601}", "pid": #{w.pid}, "index": #{w.index}, "phase": #{w.phase}, "booted": #{w.booted?}, "last_checkin": "#{w.last_checkin.utc.iso8601}", "last_status": #{w.last_status} }!}.join(",") + ']'
383
+ %Q!{ "started_at": "#{@started_at.utc.iso8601}", "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
342
384
  end
343
385
 
344
386
  def preload?
@@ -372,7 +414,13 @@ module Puma
372
414
  log "Early termination of worker"
373
415
  exit! 0
374
416
  else
417
+ @launcher.close_binder_listeners
418
+
419
+ stop_workers
375
420
  stop
421
+
422
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
423
+ exit 0 # Clean exit, workers were stopped
376
424
  end
377
425
  end
378
426
  end
@@ -466,7 +514,7 @@ module Puma
466
514
 
467
515
  force_check = false
468
516
 
469
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
517
+ res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
470
518
 
471
519
  if res
472
520
  req = read.read_nonblock(1)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  # Rack::CommonLogger forwards every request to the given +app+, and
3
5
  # logs a line in the
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/rack/builder'
2
4
  require 'puma/plugin'
3
5
  require 'puma/const'
@@ -13,152 +15,147 @@ module Puma
13
15
  DefaultWorkerShutdownTimeout = 30
14
16
  end
15
17
 
16
- class LeveledOptions
17
- def initialize(default_options, user_options)
18
- @cur = user_options
19
- @set = [@cur]
20
- @defaults = default_options.dup
21
- end
22
-
23
- def initialize_copy(other)
24
- @set = @set.map { |o| o.dup }
25
- @cur = @set.last
26
- end
27
-
28
- def shift
29
- @cur = {}
30
- @set << @cur
31
- end
32
-
33
- def reverse_shift
34
- @cur = {}
35
- @set.unshift(@cur)
36
- end
18
+ # A class used for storing "leveled" configuration options.
19
+ #
20
+ # In this class any "user" specified options take precedence over any
21
+ # "file" specified options, take precedence over any "default" options.
22
+ #
23
+ # User input is preferred over "defaults":
24
+ # user_options = { foo: "bar" }
25
+ # default_options = { foo: "zoo" }
26
+ # options = UserFileDefaultOptions.new(user_options, default_options)
27
+ # puts options[:foo]
28
+ # # => "bar"
29
+ #
30
+ # All values can be accessed via `all_of`
31
+ #
32
+ # puts options.all_of(:foo)
33
+ # # => ["bar", "zoo"]
34
+ #
35
+ # A "file" option can be set. This config will be preferred over "default" options
36
+ # but will defer to any available "user" specified options.
37
+ #
38
+ # user_options = { foo: "bar" }
39
+ # default_options = { rackup: "zoo.rb" }
40
+ # options = UserFileDefaultOptions.new(user_options, default_options)
41
+ # options.file_options[:rackup] = "sup.rb"
42
+ # puts options[:rackup]
43
+ # # => "sup.rb"
44
+ #
45
+ # The "default" options can be set via procs. These are resolved during runtime
46
+ # via calls to `finalize_values`
47
+ class UserFileDefaultOptions
48
+ def initialize(user_options, default_options)
49
+ @user_options = user_options
50
+ @file_options = {}
51
+ @default_options = default_options
52
+ end
53
+
54
+ attr_reader :user_options, :file_options, :default_options
37
55
 
38
56
  def [](key)
39
- @set.reverse_each do |o|
40
- if o.key? key
41
- return o[key]
42
- end
43
- end
44
-
45
- v = @defaults[key]
46
- if v.respond_to? :call
47
- v.call
48
- else
49
- v
50
- end
51
- end
52
-
53
- def fetch(key, default=nil)
54
- val = self[key]
55
- return val if val
56
- default
57
- end
58
-
59
- attr_reader :cur
60
-
61
- def all_of(key)
62
- all = []
63
-
64
- @set.each do |o|
65
- if v = o[key]
66
- if v.kind_of? Array
67
- all += v
68
- else
69
- all << v
70
- end
71
- end
72
- end
73
-
74
- all
75
- end
76
-
77
- def []=(key, val)
78
- @cur[key] = val
57
+ return user_options[key] if user_options.key?(key)
58
+ return file_options[key] if file_options.key?(key)
59
+ return default_options[key] if default_options.key?(key)
79
60
  end
80
61
 
81
- def key?(key)
82
- @set.each do |o|
83
- if o.key? key
84
- return true
85
- end
86
- end
87
-
88
- @default.key? key
62
+ def []=(key, value)
63
+ user_options[key] = value
89
64
  end
90
65
 
91
- def merge!(o)
92
- o.each do |k,v|
93
- @cur[k]= v
94
- end
66
+ def fetch(key, default_value = nil)
67
+ self[key] || default_value
95
68
  end
96
69
 
97
- def flatten
98
- options = {}
99
-
100
- @set.each do |o|
101
- o.each do |k,v|
102
- options[k] ||= v
103
- end
104
- end
105
-
106
- options
107
- end
70
+ def all_of(key)
71
+ user = user_options[key]
72
+ file = file_options[key]
73
+ default = default_options[key]
108
74
 
109
- def explain
110
- indent = ""
75
+ user = [user] unless user.is_a?(Array)
76
+ file = [file] unless file.is_a?(Array)
77
+ default = [default] unless default.is_a?(Array)
111
78
 
112
- @set.each do |o|
113
- o.keys.sort.each do |k|
114
- puts "#{indent}#{k}: #{o[k].inspect}"
115
- end
79
+ user.compact!
80
+ file.compact!
81
+ default.compact!
116
82
 
117
- indent = " #{indent}"
118
- end
83
+ user + file + default
119
84
  end
120
85
 
121
- def force_defaults
122
- @defaults.each do |k,v|
86
+ def finalize_values
87
+ @default_options.each do |k,v|
123
88
  if v.respond_to? :call
124
- @defaults[k] = v.call
89
+ @default_options[k] = v.call
125
90
  end
126
91
  end
127
92
  end
128
93
  end
129
94
 
95
+ # The main configuration class of Puma.
96
+ #
97
+ # It can be initialized with a set of "user" options and "default" options.
98
+ # Defaults will be merged with `Configuration.puma_default_options`.
99
+ #
100
+ # This class works together with 2 main other classes the `UserFileDefaultOptions`
101
+ # which stores configuration options in order so the precedence is that user
102
+ # set configuration wins over "file" based configuration wins over "default"
103
+ # configuration. These configurations are set via the `DSL` class. This
104
+ # class powers the Puma config file syntax and does double duty as a configuration
105
+ # DSL used by the `Puma::CLI` and Puma rack handler.
106
+ #
107
+ # It also handles loading plugins.
108
+ #
109
+ # > Note: `:port` and `:host` are not valid keys. By they time they make it to the
110
+ # configuration options they are expected to be incorporated into a `:binds` key.
111
+ # Under the hood the DSL maps `port` and `host` calls to `:binds`
112
+ #
113
+ # config = Configuration.new({}) do |user_config, file_config, default_config|
114
+ # user_config.port 3003
115
+ # end
116
+ # config.load
117
+ # puts config.options[:port]
118
+ # # => 3003
119
+ #
120
+ # It is expected that `load` is called on the configuration instance after setting
121
+ # config. This method expands any values in `config_file` and puts them into the
122
+ # correct configuration option hash.
123
+ #
124
+ # Once all configuration is complete it is expected that `clamp` will be called
125
+ # on the instance. This will expand any procs stored under "default" values. This
126
+ # is done because an environment variable may have been modified while loading
127
+ # configuration files.
130
128
  class Configuration
131
129
  include ConfigDefault
132
130
 
133
- def self.from_file(path)
134
- cfg = new
131
+ def initialize(user_options={}, default_options = {}, &block)
132
+ default_options = self.puma_default_options.merge(default_options)
135
133
 
136
- DSL.new(cfg.options, cfg)._load_from path
134
+ @options = UserFileDefaultOptions.new(user_options, default_options)
135
+ @plugins = PluginLoader.new
136
+ @user_dsl = DSL.new(@options.user_options, self)
137
+ @file_dsl = DSL.new(@options.file_options, self)
138
+ @default_dsl = DSL.new(@options.default_options, self)
137
139
 
138
- return cfg
139
- end
140
-
141
- def initialize(options={}, &blk)
142
- @options = LeveledOptions.new(default_options, options)
143
-
144
- @plugins = PluginLoader.new
145
-
146
- if blk
147
- configure(&blk)
140
+ if block
141
+ configure(&block)
148
142
  end
149
143
  end
150
144
 
151
145
  attr_reader :options, :plugins
152
146
 
153
- def configure(&blk)
154
- @options.shift
155
- DSL.new(@options, self)._run(&blk)
147
+ def configure
148
+ yield @user_dsl, @file_dsl, @default_dsl
149
+ ensure
150
+ @user_dsl._offer_plugins
151
+ @file_dsl._offer_plugins
152
+ @default_dsl._offer_plugins
156
153
  end
157
154
 
158
155
  def initialize_copy(other)
159
- @conf = nil
156
+ @conf = nil
160
157
  @cli_options = nil
161
- @options = @options.dup
158
+ @options = @options.dup
162
159
  end
163
160
 
164
161
  def flatten
@@ -170,7 +167,7 @@ module Puma
170
167
  self
171
168
  end
172
169
 
173
- def default_options
170
+ def puma_default_options
174
171
  {
175
172
  :min_threads => 0,
176
173
  :max_threads => 16,
@@ -185,39 +182,38 @@ module Puma
185
182
  :worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
186
183
  :remote_address => :socket,
187
184
  :tag => method(:infer_tag),
188
- :environment => lambda { ENV['RACK_ENV'] || "development" },
185
+ :environment => -> { ENV['RACK_ENV'] || "development" },
189
186
  :rackup => DefaultRackup,
190
187
  :logger => STDOUT,
191
- :persistent_timeout => Const::PERSISTENT_TIMEOUT
188
+ :persistent_timeout => Const::PERSISTENT_TIMEOUT,
189
+ :first_data_timeout => Const::FIRST_DATA_TIMEOUT,
190
+ :raise_exception_on_sigterm => true
192
191
  }
193
192
  end
194
193
 
195
194
  def load
196
- files = @options.all_of(:config_files)
195
+ config_files.each { |config_file| @file_dsl._load_from(config_file) }
197
196
 
198
- if files.empty?
199
- imp = %W(config/puma/#{@options[:environment]}.rb config/puma.rb).find { |f|
200
- File.exist?(f)
201
- }
197
+ @options
198
+ end
202
199
 
203
- files << imp
204
- elsif files == ["-"]
205
- files = []
206
- end
200
+ def config_files
201
+ files = @options.all_of(:config_files)
207
202
 
208
- files.each do |f|
209
- @options.reverse_shift
203
+ return [] if files == ['-']
204
+ return files if files.any?
210
205
 
211
- DSL.load @options, self, f
206
+ first_default_file = %W(config/puma/#{environment_str}.rb config/puma.rb).find do |f|
207
+ File.exist?(f)
212
208
  end
213
- @options.shift
209
+
210
+ [first_default_file]
214
211
  end
215
212
 
216
213
  # Call once all configuration (included from rackup files)
217
214
  # is loaded to flesh out any defaults
218
215
  def clamp
219
- @options.shift
220
- @options.force_defaults
216
+ @options.finalize_values
221
217
  end
222
218
 
223
219
  # Injects the Configuration object into the env
@@ -271,6 +267,10 @@ module Puma
271
267
  @options[:environment]
272
268
  end
273
269
 
270
+ def environment_str
271
+ environment.respond_to?(:call) ? environment.call : environment
272
+ end
273
+
274
274
  def load_plugin(name)
275
275
  @plugins.create name
276
276
  end
@@ -318,17 +318,15 @@ module Puma
318
318
  def load_rackup
319
319
  raise "Missing rackup file '#{rackup}'" unless File.exist?(rackup)
320
320
 
321
- @options.shift
322
-
323
321
  rack_app, rack_options = rack_builder.parse_file(rackup)
324
- @options.merge!(rack_options)
322
+ @options.file_options.merge!(rack_options)
325
323
 
326
324
  config_ru_binds = []
327
325
  rack_options.each do |k, v|
328
326
  config_ru_binds << v if k.to_s.start_with?("bind")
329
327
  end
330
328
 
331
- @options[:binds] = config_ru_binds unless config_ru_binds.empty?
329
+ @options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
332
330
 
333
331
  rack_app
334
332
  end
@@ -350,7 +348,7 @@ module Puma
350
348
  end
351
349
 
352
350
  if bytes
353
- token = ""
351
+ token = "".dup
354
352
  bytes.each_byte { |b| token << b.to_s(16) }
355
353
  else
356
354
  token = (0..count).to_a.map { rand(255).to_s(16) }.join