capistrano3-puma 3.1.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,6 +1,17 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/capistrano3-puma.svg)](http://badge.fury.io/rb/capistrano3-puma)
2
2
  # Capistrano::Puma
3
3
 
4
+ Puma integration for Capistrano - providing systemd service management and zero-downtime deployments for Puma 5.1+.
5
+
6
+ ## Example Application
7
+
8
+ For a complete working example of this gem in action, see the [capistrano-example-app](https://github.com/seuros/capistrano-example-app) which demonstrates:
9
+ - Rails 8.0 deployment with Capistrano
10
+ - Puma 6.0 with systemd socket activation
11
+ - Zero-downtime deployments
12
+ - rbenv integration
13
+ - Nginx configuration examples
14
+
4
15
  ## Installation
5
16
 
6
17
  Add this line to your application's Gemfile:
@@ -15,92 +26,147 @@ And then execute:
15
26
 
16
27
  $ bundle
17
28
 
29
+ ## Upgrading from Version 5.x to 6.x
30
+
31
+ Version 6.0 includes several breaking changes:
32
+
33
+ ### Breaking Changes:
34
+ 1. **Manual Puma Configuration**: The gem no longer generates `puma.rb` automatically
35
+ - You must create your own `config/puma.rb` in your repository
36
+ - Add it to `linked_files` in your `deploy.rb`
37
+
38
+ 2. **Default puma_bind Removed**: You must explicitly set the bind address
39
+ ```ruby
40
+ set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock"
41
+ ```
42
+
43
+ 3. **Systemd Service Changes**: Services are now user-specific by default
44
+ - Services are created in `~/.config/systemd/user/`
45
+ - Run `cap production puma:install` again after upgrading
46
+
47
+ ### Migration Steps:
48
+ 1. Create your `config/puma.rb` if you don't have one
49
+ 2. Add to your `deploy.rb`:
50
+ ```ruby
51
+ append :linked_files, 'config/puma.rb'
52
+ set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock"
53
+ ```
54
+ 3. Run `cap production puma:install` to update the systemd service
55
+ 4. Deploy as normal
56
+
18
57
  ## Usage
19
58
  ```ruby
20
59
  # Capfile
21
60
 
22
61
  require 'capistrano/puma'
23
62
  install_plugin Capistrano::Puma # Default puma tasks
24
- install_plugin Capistrano::Puma::Workers # if you want to control the workers (in cluster mode)
25
- install_plugin Capistrano::Puma::Jungle # if you need the jungle tasks
26
- install_plugin Capistrano::Puma::Monit # if you need the monit tasks
27
- install_plugin Capistrano::Puma::Nginx # if you want to upload a nginx site template
63
+ install_plugin Capistrano::Puma::Systemd
28
64
  ```
29
65
 
30
66
  To prevent loading the hooks of the plugin, add false to the load_hooks param.
31
67
  ```ruby
32
68
  # Capfile
33
69
 
34
- require 'capistrano/puma'
35
70
  install_plugin Capistrano::Puma, load_hooks: false # Default puma tasks without hooks
36
- install_plugin Capistrano::Puma::Monit, load_hooks: false # Monit tasks without hooks
71
+ ```
72
+
73
+ To make it work with rvm, rbenv and chruby, install the plugin after corresponding library inclusion.
74
+ ```ruby
75
+ # Capfile
76
+
77
+ require 'capistrano/rbenv'
78
+ require 'capistrano/puma'
79
+ install_plugin Capistrano::Puma
37
80
  ```
38
81
 
39
82
  ### Config
40
83
 
41
- To list available tasks use `cap -T`
84
+ Puma configuration is expected to be in `config/puma.rb` or `config/puma/#{fetch(:puma_env)}.rb` and checked in your repository.
42
85
 
43
- To upload puma config use:
44
- ```ruby
45
- cap production puma:config
46
- ```
47
- By default the file located in `shared/puma.config`
86
+ Starting with version 6.0.0, you need to manage the puma configuration file yourself. Here are the steps:
48
87
 
88
+ 1. Create your puma configuration in `shared/config/puma.rb` on the server
89
+ 2. Add it to linked_files in your `deploy.rb`:
90
+ ```ruby
91
+ append :linked_files, 'config/puma.rb'
92
+ ```
49
93
 
50
- Ensure that `tmp/pids` and ` tmp/sockets log` are shared (via `linked_dirs`):
94
+ This ensures the puma configuration persists across deployments. The systemd service will start puma with `puma -e <environment>` from your app's current directory.
51
95
 
52
- `This step is mandatory before deploying, otherwise puma server won't start`
96
+ ### First-Time Setup (IMPORTANT! 🎉)
53
97
 
54
- ### Nginx
98
+ **🙋 Hey there, human! Read this magical section and save yourself from confusion! 😊**
55
99
 
56
- To upload a nginx site config (eg. /etc/nginx/sites-enabled/) use:
57
- ```ruby
58
- cap production puma:nginx_config
100
+ Before your first deployment, you MUST install the Puma systemd service on your server:
101
+
102
+ ```bash
103
+ # ✨ This only needs to be done once per server/stage - it's like a first date! 💝
104
+ $ bundle exec cap production puma:install
59
105
  ```
60
106
 
61
- To customize these two templates locally before uploading use:
107
+ This command will:
108
+ - 🏗️ Create the systemd service files for Puma
109
+ - 🚀 Enable the service to start on boot
110
+ - 🔐 Set up the proper user permissions
111
+
112
+ **🎭 Plot twist:** Without running this command first, your deployment will succeed but Puma won't start! (We know, it's sneaky like that 😅)
113
+
114
+ ### Deployment
115
+
116
+ After the initial setup, normal deployments will work as expected:
117
+ ```bash
118
+ $ bundle exec cap production deploy
62
119
  ```
63
- rails g capistrano:nginx_puma:config
120
+
121
+ The deployment process will automatically restart Puma using the installed systemd service.
122
+
123
+ To manually control the Puma service:
124
+ ```bash
125
+ $ bundle exec cap production puma:start
126
+ $ bundle exec cap production puma:stop
127
+ $ bundle exec cap production puma:restart
64
128
  ```
65
129
 
66
- if your nginx server configuration is not located in `/etc/nginx`, you may need to customize:
67
- ```ruby
68
- set :nginx_sites_available_path, "/etc/nginx/sites-available"
69
- set :nginx_sites_enabled_path, "/etc/nginx/sites-enabled"
130
+ To uninstall the systemd service:
131
+ ```bash
132
+ $ bundle exec cap production puma:uninstall
70
133
  ```
71
134
 
72
- By default, `nginx_config` will be executed with `:web` role. But you can assign it to a different role:
73
- ```ruby
74
- set :puma_nginx, :foo
135
+ ### Full Task List
75
136
  ```
76
- or define a standalone one:
77
- ```ruby
78
- role :puma_nginx, %w{root@example.com}
137
+ $ cap -T puma
138
+ cap puma:disable # Disable Puma systemd service
139
+ cap puma:enable # Enable Puma systemd service
140
+ cap puma:install # Install Puma systemd service
141
+ cap puma:reload # Reload Puma service via systemd
142
+ cap puma:restart # Restart Puma service via systemd
143
+ cap puma:restart_socket # Restart Puma socket via systemd
144
+ cap puma:smart_restart # Restarts or reloads Puma service via systemd
145
+ cap puma:start # Start Puma service via systemd
146
+ cap puma:status # Get Puma service status via systemd
147
+ cap puma:stop # Stop Puma service via systemd
148
+ cap puma:stop_socket # Stop Puma socket via systemd
149
+ cap puma:uninstall # Uninstall Puma systemd service
79
150
  ```
151
+ ## Example
80
152
 
81
- ### Jungle
153
+ A sample application is provided to show how to use this gem at https://github.com/seuros/capistrano-example-app
82
154
 
83
- For Jungle tasks (beta), these options exist:
84
- ```ruby
85
- set :puma_jungle_conf, '/etc/puma.conf'
86
- set :puma_run_path, '/usr/local/bin/run-puma'
87
- ```
155
+ ### Systemd Socket Activation
88
156
 
89
- ### Multi bind
157
+ Systemd socket activation starts your app upon first request if it is not already running
90
158
 
91
- Multi-bind can be set with an array in the puma_bind variable
92
159
  ```ruby
93
- set :puma_bind, %w(tcp://0.0.0.0:9292 unix:///tmp/puma.sock)
160
+ set :puma_enable_socket_service, true
94
161
  ```
95
- * Listening on tcp://0.0.0.0:9220
96
- * Listening on unix:///tmp/puma.sock
97
162
 
98
- ### Active Record
163
+ For more information on socket activation have a look at the `systemd.socket` [man page](https://man7.org/linux/man-pages/man5/systemd.socket.5.html).
99
164
 
100
- For ActiveRecord the following line to your deploy.rb
101
- ```ruby
102
- set :puma_init_active_record, true
165
+ To restart the listening socket using Systemd run
103
166
  ```
167
+ cap puma:systemd:restart_socket
168
+ ```
169
+ This would also restart the puma instance as the puma service depends on the socket service being active
104
170
 
105
171
  ### Other configs
106
172
 
@@ -108,36 +174,12 @@ Configurable options, shown here with defaults: Please note the configuration op
108
174
 
109
175
  ```ruby
110
176
  set :puma_user, fetch(:user)
111
- set :puma_rackup, -> { File.join(current_path, 'config.ru') }
112
- set :puma_state, "#{shared_path}/tmp/pids/puma.state"
113
- set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
114
- set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock" #accept array for multi-bind
115
- set :puma_control_app, false
116
- set :puma_default_control_app, "unix://#{shared_path}/tmp/sockets/pumactl.sock"
117
- set :puma_conf, "#{shared_path}/puma.rb"
118
- set :puma_access_log, "#{shared_path}/log/puma_access.log"
119
- set :puma_error_log, "#{shared_path}/log/puma_error.log"
120
- set :puma_role, :app
121
- set :puma_env, fetch(:rack_env, fetch(:rails_env, 'production'))
122
- set :puma_threads, [0, 16]
123
- set :puma_workers, 0
124
- set :puma_worker_timeout, nil
125
- set :puma_init_active_record, false
126
- set :puma_preload_app, false
127
- set :puma_daemonize, false
128
- set :puma_plugins, [] #accept array of plugins
129
- set :puma_tag, fetch(:application)
130
-
131
- set :nginx_config_name, "#{fetch(:application)}_#{fetch(:stage)}"
132
- set :nginx_flags, 'fail_timeout=0'
133
- set :nginx_http_flags, fetch(:nginx_flags)
134
- set :nginx_server_name, "localhost #{fetch(:application)}.local"
135
- set :nginx_sites_available_path, '/etc/nginx/sites-available'
136
- set :nginx_sites_enabled_path, '/etc/nginx/sites-enabled'
137
- set :nginx_socket_flags, fetch(:nginx_flags)
138
- set :nginx_ssl_certificate, "/etc/ssl/certs/{fetch(:nginx_config_name)}.crt"
139
- set :nginx_ssl_certificate_key, "/etc/ssl/private/{fetch(:nginx_config_name)}.key"
140
- set :nginx_use_ssl, false
177
+ set :puma_role, :web
178
+ set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock"
179
+ set :puma_systemd_watchdog_sec, 10 # Set to 0 or false to disable watchdog
180
+ set :puma_service_unit_env_files, []
181
+ set :puma_service_unit_env_vars, []
182
+ set :puma_service_unit_props, [] # Set extral puma service properties, such as ["MemoryMax=2G","TimeoutAbortSec=30"]
141
183
  ```
142
184
 
143
185
  __Notes:__ If you are setting values for variables that might be used by other plugins, use `append` instead of `set`. For example:
@@ -145,6 +187,38 @@ __Notes:__ If you are setting values for variables that might be used by other p
145
187
  append :rbenv_map_bins, 'puma', 'pumactl'
146
188
  ```
147
189
 
190
+ ## Troubleshooting
191
+
192
+ ### Puma is not starting after deployment
193
+ - Ensure you ran `cap production puma:install` before your first deployment
194
+ - Check the service status: `cap production puma:status`
195
+ - Check logs: `sudo journalctl -u your_app_puma_production -n 100`
196
+
197
+ ### Nginx 502 Bad Gateway errors
198
+ This usually means nginx and puma have mismatched configurations:
199
+ - If nginx expects a unix socket but puma binds to a port (or vice versa)
200
+ - Ensure your `puma_bind` in deploy.rb matches your nginx upstream configuration
201
+ - Common configurations:
202
+ ```ruby
203
+ # Unix socket (default)
204
+ set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock"
205
+
206
+ # TCP port
207
+ set :puma_bind, "tcp://0.0.0.0:3000"
208
+ ```
209
+
210
+ ### Puma keeps restarting (systemd watchdog kills it)
211
+ - Your app may take longer than 10 seconds to boot
212
+ - Disable or increase WatchdogSec (requires version 6.0.0+):
213
+ ```ruby
214
+ set :puma_systemd_watchdog_sec, 30 # 30 seconds
215
+ # or
216
+ set :puma_systemd_watchdog_sec, 0 # Disable watchdog
217
+ ```
218
+
219
+ # Nginx documentation
220
+ Nginx documentation was moved to [nginx.md](docs/nginx.md)
221
+
148
222
  ## Contributing
149
223
 
150
224
  1. Fork it
@@ -11,6 +11,8 @@ module Capistrano
11
11
  set_if_empty :nginx_http_flags, fetch(:nginx_flags)
12
12
  set_if_empty :nginx_socket_flags, fetch(:nginx_flags)
13
13
  set_if_empty :nginx_use_ssl, false
14
+ set_if_empty :nginx_use_http2, true
15
+ set_if_empty :nginx_downstream_uses_ssl, false
14
16
  end
15
17
 
16
18
  def define_tasks
@@ -0,0 +1,61 @@
1
+ module Capistrano
2
+ class Puma::Systemd < Capistrano::Plugin
3
+ include PumaCommon
4
+
5
+ def register_hooks
6
+ after 'deploy:published', 'puma:smart_restart'
7
+ end
8
+
9
+ def define_tasks
10
+ eval_rakefile File.expand_path('../../tasks/systemd.rake', __FILE__)
11
+ end
12
+
13
+ def set_defaults
14
+ set_if_empty :puma_systemctl_bin, -> { fetch(:systemctl_bin, '/bin/systemctl') }
15
+ set_if_empty :puma_service_unit_name, -> { "#{fetch(:application)}_puma_#{fetch(:stage)}" }
16
+ set_if_empty :puma_enable_socket_service, false
17
+ set_if_empty :puma_systemd_watchdog_sec, 10
18
+
19
+ set_if_empty :puma_service_unit_env_files, -> { fetch(:service_unit_env_files, []) }
20
+ set_if_empty :puma_service_unit_env_vars, -> { fetch(:service_unit_env_vars, []) }
21
+ set_if_empty :puma_service_unit_props, -> { fetch(:service_unit_props, []) }
22
+
23
+ set_if_empty :puma_systemctl_user, -> { fetch(:systemctl_user, :user) }
24
+ set_if_empty :puma_enable_lingering, -> { fetch(:puma_systemctl_user) != :system }
25
+ set_if_empty :puma_lingering_user, -> { fetch(:lingering_user, fetch(:user)) }
26
+
27
+ set_if_empty :puma_service_templates_path, fetch(:service_templates_path, 'config/deploy/templates')
28
+ end
29
+
30
+ def fetch_systemd_unit_path
31
+ if fetch(:puma_systemctl_user) == :system
32
+ "/etc/systemd/system/"
33
+ else
34
+ home_dir = backend.capture :pwd
35
+ File.join(home_dir, ".config", "systemd", "user")
36
+ end
37
+ end
38
+
39
+ def systemd_command(*args)
40
+ command = [fetch(:puma_systemctl_bin)]
41
+
42
+ unless fetch(:puma_systemctl_user) == :system
43
+ command << "--user"
44
+ end
45
+
46
+ command + args
47
+ end
48
+
49
+ def sudo_if_needed(*command)
50
+ if fetch(:puma_systemctl_user) == :system
51
+ backend.sudo command.map(&:to_s).join(" ")
52
+ else
53
+ backend.execute(*command)
54
+ end
55
+ end
56
+
57
+ def execute_systemd(*args)
58
+ sudo_if_needed(*systemd_command(*args))
59
+ end
60
+ end
61
+ end
@@ -1,23 +1,23 @@
1
1
  require 'capistrano/bundler'
2
- require "capistrano/plugin"
2
+ require 'capistrano/plugin'
3
3
 
4
4
  module Capistrano
5
5
  module PumaCommon
6
6
  def puma_switch_user(role, &block)
7
7
  user = puma_user(role)
8
8
  if user == role.user
9
- block.call
9
+ yield
10
10
  else
11
- backend.as user do
12
- block.call
13
- end
11
+ backend.as(user, &block)
14
12
  end
15
13
  end
16
14
 
17
15
  def puma_user(role)
18
16
  properties = role.properties
17
+ return role.user unless properties
18
+
19
19
  properties.fetch(:puma_user) || # local property for puma only
20
- fetch(:puma_user) ||
20
+ fetch(:puma_user, nil) ||
21
21
  properties.fetch(:run_as) || # global property across multiple capistrano gems
22
22
  role.user
23
23
  end
@@ -28,8 +28,14 @@ module Capistrano
28
28
  end.join("\n")
29
29
  end
30
30
 
31
+ def service_unit_type
32
+ ## Jruby don't support notify
33
+ return "simple" if RUBY_ENGINE == "jruby"
34
+ ## Puma 6.1+ has built-in sd_notify support
35
+ fetch(:puma_service_unit_type, "notify")
36
+ end
31
37
 
32
- def template_puma(from, to, role)
38
+ def compiled_template_puma(from, role)
33
39
  @role = role
34
40
  file = [
35
41
  "lib/capistrano/templates/#{from}-#{role.hostname}-#{fetch(:stage)}.rb",
@@ -45,77 +51,76 @@ module Capistrano
45
51
  File.expand_path("../templates/#{from}.rb.erb", __FILE__)
46
52
  ].detect { |path| File.file?(path) }
47
53
  erb = File.read(file)
48
- backend.upload! StringIO.new(ERB.new(erb, nil, '-').result(binding)), to
54
+ StringIO.new(ERB.new(erb, trim_mode: '-').result(binding))
49
55
  end
50
- end
51
56
 
52
- class Puma < Capistrano::Plugin
53
- include PumaCommon
54
-
55
- def define_tasks
56
- eval_rakefile File.expand_path('../tasks/puma.rake', __FILE__)
57
+ def template_puma(from, to, role)
58
+ backend.upload! compiled_template_puma(from, role), to
57
59
  end
58
60
 
59
- def set_defaults
60
- set_if_empty :puma_role, :app
61
- set_if_empty :puma_env, -> { fetch(:rack_env, fetch(:rails_env, fetch(:stage))) }
62
- # Configure "min" to be the minimum number of threads to use to answer
63
- # requests and "max" the maximum.
64
- set_if_empty :puma_threads, [0, 16]
65
- set_if_empty :puma_workers, 0
66
- set_if_empty :puma_rackup, -> { File.join(current_path, 'config.ru') }
67
- set_if_empty :puma_state, -> { File.join(shared_path, 'tmp', 'pids', 'puma.state') }
68
- set_if_empty :puma_pid, -> { File.join(shared_path, 'tmp', 'pids', 'puma.pid') }
69
- set_if_empty :puma_bind, -> { File.join("unix://#{shared_path}", 'tmp', 'sockets', 'puma.sock') }
70
- set_if_empty :puma_control_app, false
71
- set_if_empty :puma_default_control_app, -> { File.join("unix://#{shared_path}", 'tmp', 'sockets', 'pumactl.sock') }
72
- set_if_empty :puma_conf, -> { File.join(shared_path, 'puma.rb') }
73
- set_if_empty :puma_access_log, -> { File.join(shared_path, 'log', 'puma_access.log') }
74
- set_if_empty :puma_error_log, -> { File.join(shared_path, 'log', 'puma_error.log') }
75
- set_if_empty :puma_init_active_record, false
76
- set_if_empty :puma_preload_app, false
77
- set_if_empty :puma_daemonize, false
78
- set_if_empty :puma_tag, ''
61
+ PumaBind = Struct.new(:full_address, :kind, :address) do
62
+ def unix?
63
+ kind == :unix
64
+ end
79
65
 
80
- # Chruby, Rbenv and RVM integration
81
- append :chruby_map_bins, 'puma', 'pumactl'
82
- append :rbenv_map_bins, 'puma', 'pumactl'
83
- append :rvm_map_bins, 'puma', 'pumactl'
66
+ def ssl?
67
+ kind == :ssl
68
+ end
84
69
 
85
- # Bundler integration
86
- append :bundle_bins, 'puma', 'pumactl'
87
- end
70
+ def tcp
71
+ kind == :tcp || ssl?
72
+ end
88
73
 
89
- def register_hooks
90
- after 'deploy:check', 'puma:check'
91
- after 'deploy:finished', 'puma:smart_restart'
92
- end
74
+ def local
75
+ if unix?
76
+ self
77
+ else
78
+ PumaBind.new(
79
+ localize_address(full_address),
80
+ kind,
81
+ localize_address(address)
82
+ )
83
+ end
84
+ end
93
85
 
94
- def puma_workers
95
- fetch(:puma_workers, 0)
96
- end
86
+ private
97
87
 
98
- def puma_preload_app?
99
- fetch(:puma_preload_app)
88
+ def localize_address(address)
89
+ address.gsub(/0\.0\.0\.0(.+)/, "127.0.0.1\\1")
90
+ end
100
91
  end
101
92
 
102
- def puma_daemonize?
103
- fetch(:puma_daemonize)
93
+ def puma_binds
94
+ Array(fetch(:puma_bind)).map do |m|
95
+ etype, address = /(tcp|unix|ssl):\/{1,2}(.+)/.match(m).captures
96
+ PumaBind.new(m, etype.to_sym, address)
97
+ end
104
98
  end
105
99
 
106
- def puma_plugins
107
- Array(fetch(:puma_plugins)).collect do |bind|
108
- "plugin '#{bind}'"
109
- end.join("\n")
100
+ def expanded_bundle_command
101
+ backend.capture(:echo, SSHKit.config.command_map[:bundle]).strip
110
102
  end
103
+ end
111
104
 
112
- def upload_puma_rb(role)
113
- template_puma 'puma', fetch(:puma_conf), role
105
+ class Puma < Capistrano::Plugin
106
+ include PumaCommon
107
+
108
+ def set_defaults
109
+ set_if_empty :puma_role, :web
110
+ set_if_empty :puma_env, -> { fetch(:rack_env, fetch(:rails_env, fetch(:rake_env, fetch(:stage)))) }
111
+ set_if_empty :puma_bind, -> { "unix://#{File.join(shared_path, 'tmp', 'sockets', 'puma.sock')}" }
112
+ set_if_empty :puma_access_log, -> { File.join(shared_path, 'log', "puma.log") }
113
+ set_if_empty :puma_error_log, -> { File.join(shared_path, 'log', "puma_error.log") }
114
+
115
+ # Chruby, Rbenv and RVM integration
116
+ append :chruby_map_bins, 'puma', 'pumactl' if fetch(:chruby_map_bins)
117
+ append :rbenv_map_bins, 'puma', 'pumactl' if fetch(:rbenv_map_bins)
118
+ append :rvm_map_bins, 'puma', 'pumactl' if fetch(:rvm_map_bins)
119
+
120
+ # Bundler integration
121
+ append :bundle_bins, 'puma', 'pumactl'
114
122
  end
115
123
  end
116
124
  end
117
125
 
118
- require 'capistrano/puma/workers'
119
- require 'capistrano/puma/monit'
120
- require 'capistrano/puma/jungle'
121
- require 'capistrano/puma/nginx'
126
+ require 'capistrano/puma/systemd'
@@ -11,4 +11,12 @@ namespace :puma do
11
11
  end
12
12
  end
13
13
  end
14
+
15
+ desc 'Generate nginx configuration locally'
16
+ task :generate_nginx_config_locally do
17
+ fake_role = Struct.new(:hostname)
18
+ run_locally do
19
+ File.write('nginx.conf', git_plugin.compiled_template_puma("nginx_conf", fake_role.new("example.com")).string)
20
+ end
21
+ end
14
22
  end