puma 3.11.4 → 4.2.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +130 -1
  3. data/README.md +100 -44
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/restart.md +4 -2
  7. data/docs/systemd.md +27 -9
  8. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  9. data/ext/puma_http11/extconf.rb +8 -0
  10. data/ext/puma_http11/http11_parser.c +37 -62
  11. data/ext/puma_http11/http11_parser_common.rl +3 -3
  12. data/ext/puma_http11/mini_ssl.c +96 -5
  13. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  14. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +21 -4
  15. data/lib/puma/accept_nonblock.rb +7 -1
  16. data/lib/puma/app/status.rb +35 -29
  17. data/lib/puma/binder.rb +47 -11
  18. data/lib/puma/cli.rb +21 -7
  19. data/lib/puma/client.rb +227 -191
  20. data/lib/puma/cluster.rb +70 -31
  21. data/lib/puma/commonlogger.rb +2 -0
  22. data/lib/puma/configuration.rb +6 -3
  23. data/lib/puma/const.rb +24 -18
  24. data/lib/puma/control_cli.rb +33 -14
  25. data/lib/puma/convenient.rb +2 -0
  26. data/lib/puma/delegation.rb +2 -0
  27. data/lib/puma/detect.rb +2 -0
  28. data/lib/puma/dsl.rb +308 -76
  29. data/lib/puma/events.rb +6 -1
  30. data/lib/puma/io_buffer.rb +3 -6
  31. data/lib/puma/jruby_restart.rb +2 -0
  32. data/lib/puma/launcher.rb +102 -55
  33. data/lib/puma/minissl.rb +41 -19
  34. data/lib/puma/null_io.rb +2 -0
  35. data/lib/puma/plugin/tmp_restart.rb +2 -0
  36. data/lib/puma/plugin.rb +7 -2
  37. data/lib/puma/rack/builder.rb +4 -1
  38. data/lib/puma/rack/urlmap.rb +2 -0
  39. data/lib/puma/rack_default.rb +2 -0
  40. data/lib/puma/reactor.rb +220 -34
  41. data/lib/puma/runner.rb +14 -4
  42. data/lib/puma/server.rb +82 -40
  43. data/lib/puma/single.rb +15 -3
  44. data/lib/puma/state_file.rb +2 -0
  45. data/lib/puma/tcp_logger.rb +2 -0
  46. data/lib/puma/thread_pool.rb +59 -36
  47. data/lib/puma/util.rb +2 -6
  48. data/lib/puma.rb +8 -0
  49. data/lib/rack/handler/puma.rb +6 -3
  50. data/tools/docker/Dockerfile +16 -0
  51. data/tools/jungle/init.d/puma +6 -6
  52. data/tools/trickletest.rb +0 -1
  53. metadata +22 -10
  54. data/lib/puma/compat.rb +0 -14
  55. data/lib/puma/daemon_ext.rb +0 -31
  56. data/lib/puma/java_io_buffer.rb +0 -45
  57. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
data/lib/puma/single.rb CHANGED
@@ -1,13 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/runner'
2
4
  require 'puma/detect'
3
5
  require 'puma/plugin'
4
6
 
5
7
  module Puma
8
+ # This class is instantiated by the `Puma::Launcher` and used
9
+ # to boot and serve a Ruby application when no puma "workers" are needed
10
+ # i.e. only using "threaded" mode. For example `$ puma -t 1:5`
11
+ #
12
+ # At the core of this class is running an instance of `Puma::Server` which
13
+ # gets created via the `start_server` method from the `Puma::Runner` class
14
+ # that this inherits from.
6
15
  class Single < Runner
7
16
  def stats
8
17
  b = @server.backlog || 0
9
18
  r = @server.running || 0
10
- %Q!{ "backlog": #{b}, "running": #{r} }!
19
+ t = @server.pool_capacity || 0
20
+ m = @server.max_threads || 0
21
+ %Q!{ "started_at": "#{@started_at.utc.iso8601}", "backlog": #{b}, "running": #{r}, "pool_capacity": #{t}, "max_threads": #{m} }!
11
22
  end
12
23
 
13
24
  def restart
@@ -15,7 +26,7 @@ module Puma
15
26
  end
16
27
 
17
28
  def stop
18
- @server.stop false
29
+ @server.stop(false) if @server
19
30
  end
20
31
 
21
32
  def halt
@@ -25,7 +36,7 @@ module Puma
25
36
  def stop_blocked
26
37
  log "- Gracefully stopping, waiting for requests to finish"
27
38
  @control.stop(true) if @control
28
- @server.stop(true)
39
+ @server.stop(true) if @server
29
40
  end
30
41
 
31
42
  def jruby_daemon?
@@ -107,6 +118,7 @@ module Puma
107
118
  rescue Interrupt
108
119
  # Swallow it
109
120
  end
121
+ @launcher.close_binder_unix_paths
110
122
  end
111
123
  end
112
124
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module Puma
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  class TCPLogger
3
5
  def initialize(logger, app, quiet=false)
@@ -1,8 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  module Puma
4
- # A simple thread pool management object.
6
+ # Internal Docs for A simple thread pool management object.
7
+ #
8
+ # Each Puma "worker" has a thread pool to process requests.
5
9
  #
10
+ # First a connection to a client is made in `Puma::Server`. It is wrapped in a
11
+ # `Puma::Client` instance and then passed to the `Puma::Reactor` to ensure
12
+ # the whole request is buffered into memory. Once the request is ready, it is passed into
13
+ # a thread pool via the `Puma::ThreadPool#<<` operator where it is stored in a `@todo` array.
14
+ #
15
+ # Each thread in the pool has an internal loop where it pulls a request from the `@todo` array
16
+ # and proceses it.
6
17
  class ThreadPool
7
18
  class ForceShutdown < RuntimeError
8
19
  end
@@ -49,7 +60,7 @@ module Puma
49
60
  @clean_thread_locals = false
50
61
  end
51
62
 
52
- attr_reader :spawned, :trim_requested
63
+ attr_reader :spawned, :trim_requested, :waiting
53
64
  attr_accessor :clean_thread_locals
54
65
 
55
66
  def self.clean_thread_locals
@@ -64,6 +75,10 @@ module Puma
64
75
  @mutex.synchronize { @todo.size }
65
76
  end
66
77
 
78
+ def pool_capacity
79
+ waiting + (@max - spawned)
80
+ end
81
+
67
82
  # :nodoc:
68
83
  #
69
84
  # Must be called with @mutex held!
@@ -72,8 +87,7 @@ module Puma
72
87
  @spawned += 1
73
88
 
74
89
  th = Thread.new(@spawned) do |spawned|
75
- # Thread name is new in Ruby 2.3
76
- Thread.current.name = 'puma %03i' % spawned if Thread.current.respond_to?(:name=)
90
+ Puma.set_thread_name 'threadpool %03i' % spawned
77
91
  todo = @todo
78
92
  block = @block
79
93
  mutex = @mutex
@@ -153,16 +167,46 @@ module Puma
153
167
  end
154
168
  end
155
169
 
170
+ # This method is used by `Puma::Server` to let the server know when
171
+ # the thread pool can pull more requests from the socket and
172
+ # pass to the reactor.
173
+ #
174
+ # The general idea is that the thread pool can only work on a fixed
175
+ # number of requests at the same time. If it is already processing that
176
+ # number of requests then it is at capacity. If another Puma process has
177
+ # spare capacity, then the request can be left on the socket so the other
178
+ # worker can pick it up and process it.
179
+ #
180
+ # For example: if there are 5 threads, but only 4 working on
181
+ # requests, this method will not wait and the `Puma::Server`
182
+ # can pull a request right away.
183
+ #
184
+ # If there are 5 threads and all 5 of them are busy, then it will
185
+ # pause here, and wait until the `not_full` condition variable is
186
+ # signaled, usually this indicates that a request has been processed.
187
+ #
188
+ # It's important to note that even though the server might accept another
189
+ # request, it might not be added to the `@todo` array right away.
190
+ # For example if a slow client has only sent a header, but not a body
191
+ # then the `@todo` array would stay the same size as the reactor works
192
+ # to try to buffer the request. In tha scenario the next call to this
193
+ # method would not block and another request would be added into the reactor
194
+ # by the server. This would continue until a fully bufferend request
195
+ # makes it through the reactor and can then be processed by the thread pool.
196
+ #
197
+ # Returns the current number of busy threads, or +nil+ if shutting down.
198
+ #
156
199
  def wait_until_not_full
157
200
  @mutex.synchronize do
158
201
  while true
159
202
  return if @shutdown
160
- return if @waiting > 0
161
203
 
162
204
  # If we can still spin up new threads and there
163
- # is work queued, then accept more work until we would
205
+ # is work queued that cannot be handled by waiting
206
+ # threads, then accept more work until we would
164
207
  # spin up the max number of threads.
165
- return if @todo.size < @max - @spawned
208
+ busy_threads = @spawned - @waiting + @todo.size
209
+ return busy_threads if @max > busy_threads
166
210
 
167
211
  @not_full.wait @mutex
168
212
  end
@@ -199,10 +243,12 @@ module Puma
199
243
  end
200
244
  end
201
245
 
202
- class AutoTrim
203
- def initialize(pool, timeout)
246
+ class Automaton
247
+ def initialize(pool, timeout, thread_name, message)
204
248
  @pool = pool
205
249
  @timeout = timeout
250
+ @thread_name = thread_name
251
+ @message = message
206
252
  @running = false
207
253
  end
208
254
 
@@ -210,8 +256,9 @@ module Puma
210
256
  @running = true
211
257
 
212
258
  @thread = Thread.new do
259
+ Puma.set_thread_name @thread_name
213
260
  while @running
214
- @pool.trim
261
+ @pool.public_send(@message)
215
262
  sleep @timeout
216
263
  end
217
264
  end
@@ -224,36 +271,12 @@ module Puma
224
271
  end
225
272
 
226
273
  def auto_trim!(timeout=30)
227
- @auto_trim = AutoTrim.new(self, timeout)
274
+ @auto_trim = Automaton.new(self, timeout, "threadpool trimmer", :trim)
228
275
  @auto_trim.start!
229
276
  end
230
277
 
231
- class Reaper
232
- def initialize(pool, timeout)
233
- @pool = pool
234
- @timeout = timeout
235
- @running = false
236
- end
237
-
238
- def start!
239
- @running = true
240
-
241
- @thread = Thread.new do
242
- while @running
243
- @pool.reap
244
- sleep @timeout
245
- end
246
- end
247
- end
248
-
249
- def stop
250
- @running = false
251
- @thread.wakeup
252
- end
253
- end
254
-
255
278
  def auto_reap!(timeout=5)
256
- @reaper = Reaper.new(self, timeout)
279
+ @reaper = Automaton.new(self, timeout, "threadpool reaper", :reap)
257
280
  @reaper.start!
258
281
  end
259
282
 
data/lib/puma/util.rb CHANGED
@@ -1,10 +1,6 @@
1
- major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
1
+ # frozen_string_literal: true
2
2
 
3
- if major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
4
- require 'puma/rack/backports/uri/common_193'
5
- else
6
- require 'uri/common'
7
- end
3
+ require 'uri/common'
8
4
 
9
5
  module Puma
10
6
  module Util
data/lib/puma.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Standard libraries
2
4
  require 'socket'
3
5
  require 'tempfile'
@@ -20,4 +22,10 @@ module Puma
20
22
  def self.stats
21
23
  @get_stats.stats
22
24
  end
25
+
26
+ # Thread name is new in Ruby 2.3
27
+ def self.set_thread_name(name)
28
+ return unless Thread.current.respond_to?(:name=)
29
+ Thread.current.name = "puma #{name}"
30
+ end
23
31
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/handler'
2
4
 
3
5
  module Rack
@@ -49,6 +51,9 @@ module Rack
49
51
  self.set_host_port_to_config(host, port, user_config)
50
52
  end
51
53
 
54
+ if default_options[:Host]
55
+ file_config.set_default_host(default_options[:Host])
56
+ end
52
57
  self.set_host_port_to_config(default_options[:Host], default_options[:Port], default_config)
53
58
 
54
59
  user_config.app app
@@ -56,8 +61,6 @@ module Rack
56
61
  conf
57
62
  end
58
63
 
59
-
60
-
61
64
  def self.run(app, options = {})
62
65
  conf = self.config(app, options)
63
66
 
@@ -83,7 +86,7 @@ module Rack
83
86
  "Verbose" => "Don't report each request (default: false)"
84
87
  }
85
88
  end
86
- private
89
+
87
90
  def self.set_host_port_to_config(host, port, config)
88
91
  config.clear_binds! if host || port
89
92
 
@@ -0,0 +1,16 @@
1
+ # Use this Dockerfile to create minimal reproductions of issues
2
+
3
+ FROM ruby:2.6
4
+
5
+ # throw errors if Gemfile has been modified since Gemfile.lock
6
+ RUN bundle config --global frozen 1
7
+
8
+ WORKDIR /usr/src/app
9
+
10
+ COPY . .
11
+ RUN gem install bundler
12
+ RUN bundle install
13
+ RUN bundle exec rake compile
14
+
15
+ EXPOSE 9292
16
+ CMD bundle exec bin/puma test/rackup/hello.ru
@@ -47,11 +47,11 @@ do_start_one() {
47
47
  PIDFILE=$1/tmp/puma/pid
48
48
  if [ -e $PIDFILE ]; then
49
49
  PID=`cat $PIDFILE`
50
- # If the puma isn't running, run it, otherwise restart it.
50
+ # If the puma is running, restart it, otherwise run it.
51
51
  if ps -p $PID > /dev/null; then
52
- do_start_one_do $1
53
- else
54
52
  do_restart_one $1
53
+ else
54
+ do_start_one_do $1
55
55
  fi
56
56
  else
57
57
  do_start_one_do $1
@@ -106,8 +106,6 @@ do_stop_one() {
106
106
  if [ -e $PIDFILE ]; then
107
107
  PID=`cat $PIDFILE`
108
108
  if ps -p $PID > /dev/null; then
109
- log_daemon_msg "---> Puma $1 isn't running."
110
- else
111
109
  log_daemon_msg "---> About to kill PID `cat $PIDFILE`"
112
110
  if [ "$USE_LOCAL_BUNDLE" -eq 1 ]; then
113
111
  cd $1 && bundle exec pumactl --state $STATEFILE stop
@@ -116,6 +114,8 @@ do_stop_one() {
116
114
  fi
117
115
  # Many daemons don't delete their pidfiles when they exit.
118
116
  rm -f $PIDFILE $STATEFILE
117
+ else
118
+ log_daemon_msg "---> Puma $1 isn't running."
119
119
  fi
120
120
  else
121
121
  log_daemon_msg "---> No puma here..."
@@ -398,7 +398,7 @@ case "$1" in
398
398
  ;;
399
399
  remove)
400
400
  if [ "$#" -lt 2 ]; then
401
- echo "Please, specifiy the app's directory to remove."
401
+ echo "Please, specify the app's directory to remove."
402
402
  exit 1
403
403
  else
404
404
  do_remove $2
data/tools/trickletest.rb CHANGED
@@ -38,7 +38,6 @@ ARGV[1].to_i.times do
38
38
  do_test(st, size)
39
39
  end
40
40
 
41
- t.abort_on_exception = true
42
41
  threads << t
43
42
  end
44
43
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.11.4
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Phoenix
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-12 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2019-09-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nio4r
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
13
27
  description: Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server
14
28
  for Ruby/Rack applications. Puma is intended for use in both development and production
15
29
  environments. It's great for highly concurrent Ruby implementations such as Rubinius
@@ -51,6 +65,7 @@ files:
51
65
  - ext/puma_http11/mini_ssl.c
52
66
  - ext/puma_http11/org/jruby/puma/Http11.java
53
67
  - ext/puma_http11/org/jruby/puma/Http11Parser.java
68
+ - ext/puma_http11/org/jruby/puma/IOBuffer.java
54
69
  - ext/puma_http11/org/jruby/puma/MiniSSL.java
55
70
  - ext/puma_http11/puma_http11.c
56
71
  - lib/puma.rb
@@ -61,25 +76,21 @@ files:
61
76
  - lib/puma/client.rb
62
77
  - lib/puma/cluster.rb
63
78
  - lib/puma/commonlogger.rb
64
- - lib/puma/compat.rb
65
79
  - lib/puma/configuration.rb
66
80
  - lib/puma/const.rb
67
81
  - lib/puma/control_cli.rb
68
82
  - lib/puma/convenient.rb
69
- - lib/puma/daemon_ext.rb
70
83
  - lib/puma/delegation.rb
71
84
  - lib/puma/detect.rb
72
85
  - lib/puma/dsl.rb
73
86
  - lib/puma/events.rb
74
87
  - lib/puma/io_buffer.rb
75
- - lib/puma/java_io_buffer.rb
76
88
  - lib/puma/jruby_restart.rb
77
89
  - lib/puma/launcher.rb
78
90
  - lib/puma/minissl.rb
79
91
  - lib/puma/null_io.rb
80
92
  - lib/puma/plugin.rb
81
93
  - lib/puma/plugin/tmp_restart.rb
82
- - lib/puma/rack/backports/uri/common_193.rb
83
94
  - lib/puma/rack/builder.rb
84
95
  - lib/puma/rack/urlmap.rb
85
96
  - lib/puma/rack_default.rb
@@ -92,6 +103,7 @@ files:
92
103
  - lib/puma/thread_pool.rb
93
104
  - lib/puma/util.rb
94
105
  - lib/rack/handler/puma.rb
106
+ - tools/docker/Dockerfile
95
107
  - tools/jungle/README.md
96
108
  - tools/jungle/init.d/README.md
97
109
  - tools/jungle/init.d/puma
@@ -108,6 +120,7 @@ licenses:
108
120
  - BSD-3-Clause
109
121
  metadata:
110
122
  msys2_mingw_dependencies: openssl
123
+ changelog_uri: https://github.com/puma/puma/blob/master/History.md
111
124
  post_install_message:
112
125
  rdoc_options: []
113
126
  require_paths:
@@ -116,15 +129,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
129
  requirements:
117
130
  - - ">="
118
131
  - !ruby/object:Gem::Version
119
- version: 1.9.3
132
+ version: '2.2'
120
133
  required_rubygems_version: !ruby/object:Gem::Requirement
121
134
  requirements:
122
135
  - - ">="
123
136
  - !ruby/object:Gem::Version
124
137
  version: '0'
125
138
  requirements: []
126
- rubyforge_project:
127
- rubygems_version: 2.7.6
139
+ rubygems_version: 3.0.3
128
140
  signing_key:
129
141
  specification_version: 4
130
142
  summary: Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for
data/lib/puma/compat.rb DELETED
@@ -1,14 +0,0 @@
1
- # Provides code to work properly on 1.8 and 1.9
2
-
3
- class String
4
- unless method_defined? :bytesize
5
- alias_method :bytesize, :size
6
- end
7
-
8
- unless method_defined? :byteslice
9
- def byteslice(*arg)
10
- enc = self.encoding
11
- self.dup.force_encoding(Encoding::ASCII_8BIT).slice(*arg).force_encoding(enc)
12
- end
13
- end
14
- end
@@ -1,31 +0,0 @@
1
- module Process
2
-
3
- # This overrides the default version because it is broken if it
4
- # exists.
5
-
6
- if respond_to? :daemon
7
- class << self
8
- remove_method :daemon
9
- end
10
- end
11
-
12
- def self.daemon(nochdir=false, noclose=false)
13
- exit if fork # Parent exits, child continues.
14
-
15
- Process.setsid # Become session leader.
16
-
17
- exit if fork # Zap session leader. See [1].
18
-
19
- Dir.chdir "/" unless nochdir # Release old working directory.
20
-
21
- if !noclose
22
- STDIN.reopen File.open("/dev/null", "r")
23
-
24
- null_out = File.open "/dev/null", "w"
25
- STDOUT.reopen null_out
26
- STDERR.reopen null_out
27
- end
28
-
29
- 0
30
- end
31
- end
@@ -1,45 +0,0 @@
1
- require 'java'
2
-
3
- # Conservative native JRuby/Java implementation of IOBuffer
4
- # backed by a ByteArrayOutputStream and conversion between
5
- # Ruby String and Java bytes
6
- module Puma
7
- class JavaIOBuffer < java.io.ByteArrayOutputStream
8
- field_reader :buf
9
- end
10
-
11
- class IOBuffer
12
- BUF_DEFAULT_SIZE = 4096
13
-
14
- def initialize
15
- @buf = JavaIOBuffer.new(BUF_DEFAULT_SIZE)
16
- end
17
-
18
- def reset
19
- @buf.reset
20
- end
21
-
22
- def <<(str)
23
- bytes = str.to_java_bytes
24
- @buf.write(bytes, 0, bytes.length)
25
- end
26
-
27
- def append(*strs)
28
- strs.each { |s| self << s; }
29
- end
30
-
31
- def to_s
32
- String.from_java_bytes @buf.to_byte_array
33
- end
34
-
35
- alias_method :to_str, :to_s
36
-
37
- def used
38
- @buf.size
39
- end
40
-
41
- def capacity
42
- @buf.buf.length
43
- end
44
- end
45
- end
@@ -1,33 +0,0 @@
1
- # :stopdoc:
2
-
3
- require 'uri/common'
4
-
5
- # Issue:
6
- # http://bugs.ruby-lang.org/issues/5925
7
- #
8
- # Relevant commit:
9
- # https://github.com/ruby/ruby/commit/edb7cdf1eabaff78dfa5ffedfbc2e91b29fa9ca1
10
-
11
-
12
- module URI
13
- begin
14
- 256.times do |i|
15
- TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
16
- end
17
- TBLENCWWWCOMP_[' '] = '+'
18
- TBLENCWWWCOMP_.freeze
19
-
20
- 256.times do |i|
21
- h, l = i>>4, i&15
22
- TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
23
- TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
24
- TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
25
- TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
26
- end
27
- TBLDECWWWCOMP_['+'] = ' '
28
- TBLDECWWWCOMP_.freeze
29
- rescue Exception
30
- end
31
- end
32
-
33
- # :startdoc: