capistrano-cable 0.1.2 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e1a47bf59656cff6458c37414595512d8fee87ad11ce021fa959f55d8ff8d96
4
- data.tar.gz: 8760656360a1e0e92413131b47e125f0ae0d00c7421896955644ca210767f502
3
+ metadata.gz: b954ccd5824a22b8706c42c8e3935ad46d6bc57c2adf3a5ddb28bd75b1908b5a
4
+ data.tar.gz: c405063b580674e99877061c12476db5ee5300a23814adae59ba624797c45727
5
5
  SHA512:
6
- metadata.gz: 2603c3875d4e470ef7c27d3360f7904584add2ca644f209d74d04eab98411c3f802ba91f8df1949965ebf809cea3c21a921562a4fc271304ad79aeee19abde7a
7
- data.tar.gz: f4f5d9d77e6800ee48a958519d7554e906fefbbcb5b9291dfb293ff8adfddb0cc07f71d744bae5173fbe4591b6e42fa776496cee9f3a00c59dab6408ed169a22
6
+ metadata.gz: 4c33e8732a80681cbbc2d2fbe6a0ddf3c509321a45be8ce6668bdd9ecacdd3c8eca8c7330c74500f6e7a684d478566a998a86d24eddd4c0b97072c9527bda024
7
+ data.tar.gz: 6712410d5ae24f5744e966bf800294784a72e9c998170105ce885e9a4381252fef2e511d3b146e7cb2630e6eec2e94d56d0382b722e59d6316ca736856aa31a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-09-15
4
+
5
+ - Replace `cable_port`, `cable_ssl_certificate` and `cable_ssl_certificate_key` with a single `cable_bind` option, defaulting to a unix socket in the shared directory
6
+ - Enable systemd socket activation by default, so the listening socket survives a restart of the server
7
+ - Install the systemd units at every deploy, before restarting the server
8
+ - Stop the deploy on `deploy:starting` when a removed option is still set, instead of moving the server somewhere else in silence
9
+
3
10
  ## [0.1.0] - 2024-07-05
4
11
 
5
12
  - Initial release
data/README.md CHANGED
@@ -44,9 +44,8 @@ Many options are available to customize the cable server configuration. Here are
44
44
  ```ruby
45
45
  # config/deploy.rb or config/deploy/<stage>.rb
46
46
  set :cable_role, :web
47
- set :cable_port, 29292
48
- # set :cable_ssl_certificate
49
- # set :cable_ssl_certificate_key
47
+ set :cable_bind, -> { "unix://#{shared_path.join("tmp", "sockets", "cable.sock")}" }
48
+ # set :cable_limit_nofile, 65536 # optional, to customize if `Errno::EMFILE: Too many open files` happens
50
49
  set :cable_rackup_file, 'cable/config.ru'
51
50
  set :cable_dir, -> { File.join(release_path, "cable") }
52
51
  set :cable_pidfile, -> { File.join(shared_path, "tmp", "pids", "cable.pid") }
@@ -54,14 +53,66 @@ set :cable_env, -> { fetch(:rack_env, fetch(:rails_env, fetch(:stage))) }
54
53
  set :cable_access_log, -> { File.join(shared_path, "log", "cable.access.log") }
55
54
  set :cable_error_log, -> { File.join(shared_path, "log", "cable.error.log") }
56
55
  set :cable_phased_restart, -> { true }
56
+ set :cable_enable_socket_service, true
57
57
  set :cable_service_unit_env_files, -> { fetch(:service_unit_env_files, []) }
58
58
  set :cable_service_unit_env_vars, -> { fetch(:service_unit_env_vars, []) }
59
59
  set :cable_service_templates_path, fetch(:service_templates_path, "config/deploy/templates")
60
60
  ```
61
61
  See Capistrao::Cable::Systemd#set_defaults for more details.
62
62
 
63
- To enable SSL, set the `cable_ssl_certificate` and `cable_ssl_certificate_key` options.
64
- The both are required to enable SSL.
63
+ ### Where the server listens
64
+
65
+ `cable_bind` is the only option about listening. It takes a Puma bind string, or
66
+ an array of them:
67
+
68
+ ```ruby
69
+ set :cable_bind, "unix:///home/myapp/public_html/shared/tmp/sockets/cable.sock"
70
+ set :cable_bind, "tcp://0.0.0.0:28090"
71
+ set :cable_bind, "ssl://0.0.0.0:28090?cert=/path/cert.pem&key=/path/key.pem"
72
+ ```
73
+
74
+ By default the server listens on a unix socket in the shared directory, which
75
+ keeps it unreachable from the outside and saves picking a free port on every
76
+ host. The socket directory is created by `cable:install`; the web server in
77
+ front then proxies to that socket path instead of a host and port, the way it
78
+ proxies to any other unix socket.
79
+
80
+ A unix socket path has to be absolute: write `unix:///path/to/cable.sock`, with
81
+ three slashes.
82
+
83
+ ### Socket activation
84
+
85
+ With `cable_enable_socket_service` (enabled by default), systemd owns the
86
+ listening socket and hands it over to Puma. The socket stays open while the
87
+ service restarts, so connections are queued in the backlog instead of being
88
+ refused, and the server is started on the first request if it is not running
89
+ yet.
90
+
91
+ The `ListenStream` of the socket unit is the address of each `cable_bind`: Puma
92
+ matches the socket it receives against its own binds, so both are always
93
+ written from the same option.
94
+
95
+ ## Migrating to 0.2.0
96
+
97
+ `cable_port`, `cable_ssl_certificate` and `cable_ssl_certificate_key` are gone,
98
+ replaced by `cable_bind`. A deploy that still sets one of them stops on
99
+ `deploy:starting`, before anything is uploaded, with a message telling what to
100
+ write instead. `cable:install` refuses to run too, for setups that install the
101
+ plugin without its hooks.
102
+
103
+ Nothing else to do on the Capistrano side: the systemd units are now installed
104
+ at every deploy, before the server is restarted, so upgrading the gem and
105
+ deploying is enough to move an app to its socket.
106
+
107
+ The one manual step is the web server, which has to proxy to the socket instead
108
+ of the port. If both can't be changed in the same window, keep the port for
109
+ now:
110
+
111
+ ```ruby
112
+ set :cable_bind, "tcp://0.0.0.0:28090"
113
+ ```
114
+
115
+ and move to the default socket in a later deploy.
65
116
 
66
117
  ## Development
67
118
 
@@ -1,34 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Capistrano
2
4
  module Cable
3
- class Bind < Struct.new(:full_address, :kind, :address)
4
- def unix?
5
- kind == :unix
6
- end
5
+ # A Puma bind, as given to `set :cable_bind`:
6
+ #
7
+ # unix:///home/app/shared/tmp/sockets/cable.sock
8
+ # tcp://0.0.0.0:28090
9
+ # ssl://0.0.0.0:28090?cert=/path/cert.pem&key=/path/key.pem
10
+ #
11
+ # `address` is the part systemd needs for its `ListenStream`: Puma matches
12
+ # the socket handed over by systemd against its own binds by address, so
13
+ # both have to be written exactly the same way.
14
+ class Bind
15
+ SCHEMES = %w[tcp ssl unix].freeze
7
16
 
8
- def ssl?
9
- kind == :ssl
10
- end
17
+ attr_reader :address
11
18
 
12
- def tcp
13
- kind == :tcp || ssl?
14
- end
19
+ def initialize(bind)
20
+ @bind = bind.to_s
21
+ scheme, rest = @bind.split(":", 2)
22
+ @scheme = scheme
23
+ @address = rest.to_s.delete_prefix("//").split("?").first.to_s
15
24
 
16
- def local
17
- if unix?
18
- self
19
- else
20
- self.class.new(
21
- localize_address(full_address),
22
- kind,
23
- localize_address(address)
24
- )
25
- end
25
+ raise ArgumentError, "Unsupported cable_bind #{@bind.inspect}, expected #{SCHEMES.join("://, ")}://" unless SCHEMES.include?(@scheme)
26
+ raise ArgumentError, "Empty address in cable_bind #{@bind.inspect}" if @address.empty?
27
+ raise ArgumentError, "cable_bind #{@bind.inspect} needs an absolute socket path" if unix? && !@address.start_with?("/")
26
28
  end
27
29
 
28
- private
30
+ def unix?
31
+ @scheme == "unix"
32
+ end
29
33
 
30
- def localize_address(address)
31
- address.gsub(/0\.0\.0\.0(.+)/, "127.0.0.1\\1")
34
+ def to_s
35
+ @bind
32
36
  end
33
37
  end
34
38
  end
@@ -1,10 +1,23 @@
1
1
  require "capistrano/plugin"
2
+ require "erb"
3
+ require "stringio"
2
4
  require_relative "bind"
3
5
 
4
6
  module Capistrano
5
7
  module Cable
6
8
  class Systemd < Capistrano::Plugin
9
+ # Options dropped in favour of :cable_bind. They are still looked up so
10
+ # that a stale setting stops the deploy before it does anything, instead
11
+ # of silently moving the server to another address.
12
+ REMOVED_OPTIONS = {
13
+ cable_port: 'set :cable_bind, "tcp://0.0.0.0:<port>"',
14
+ cable_ssl_certificate: 'set :cable_bind, "ssl://0.0.0.0:<port>?cert=<cert>&key=<key>"',
15
+ cable_ssl_certificate_key: 'set :cable_bind, "ssl://0.0.0.0:<port>?cert=<cert>&key=<key>"'
16
+ }.freeze
17
+
7
18
  def register_hooks
19
+ before "deploy:starting", "cable:check"
20
+ after "deploy:finished", "cable:install"
8
21
  after "deploy:finished", "cable:smart_restart"
9
22
  end
10
23
 
@@ -14,8 +27,8 @@ module Capistrano
14
27
 
15
28
  def set_defaults
16
29
  set_if_empty :cable_role, :web
17
- set_if_empty :cable_port, 29292
18
- set_if_empty :cable_rackup_file, 'cable/config.ru'
30
+ set_if_empty :cable_bind, -> { "unix://#{shared_path.join("tmp", "sockets", "cable.sock")}" }
31
+ set_if_empty :cable_rackup_file, "cable/config.ru"
19
32
  set_if_empty :cable_dir, -> { File.join(release_path, "cable") }
20
33
  set_if_empty :cable_pidfile, -> { File.join(shared_path, "tmp", "pids", "cable.pid") }
21
34
  set_if_empty :cable_env, -> { fetch(:rack_env, fetch(:rails_env, fetch(:stage))) }
@@ -25,9 +38,8 @@ module Capistrano
25
38
 
26
39
  set_if_empty :cable_systemctl_bin, -> { fetch(:systemctl_bin, "/bin/systemctl") }
27
40
  set_if_empty :cable_service_unit_name, -> { "#{fetch(:application)}_cable_#{fetch(:stage)}" }
28
- set_if_empty :cable_enable_socket_service, false
41
+ set_if_empty :cable_enable_socket_service, true
29
42
  set_if_empty :cable_socket_unit_name, -> { "#{fetch(:application)}_cable_#{fetch(:stage)}.socket" }
30
- # set_if_empty :cable_bind, -> { "unix:/tmp/#{fetch(:app_domain)}.sock" }
31
43
 
32
44
  set_if_empty :cable_service_unit_env_files, -> { fetch(:service_unit_env_files, []) }
33
45
  set_if_empty :cable_service_unit_env_vars, -> { fetch(:service_unit_env_vars, []) }
@@ -47,6 +59,13 @@ module Capistrano
47
59
  append :bundle_bins, "puma", "pumactl"
48
60
  end
49
61
 
62
+ def check_removed_options!
63
+ messages = REMOVED_OPTIONS.select { |option, _| fetch(option) }.map do |option, replacement|
64
+ ":#{option} is not supported anymore, use :cable_bind instead (#{replacement})"
65
+ end
66
+ raise ArgumentError, messages.join("\n") if messages.any?
67
+ end
68
+
50
69
  def expanded_bundle_command
51
70
  backend.capture(:echo, SSHKit.config.command_map[:bundle]).strip
52
71
  end
@@ -72,7 +91,7 @@ module Capistrano
72
91
 
73
92
  def sudo_if_needed(*command)
74
93
  if fetch(:cable_systemctl_user) == :system
75
- backend.sudo command.map(&:to_s).join(" ")
94
+ backend.sudo command.join(" ")
76
95
  else
77
96
  backend.execute(*command)
78
97
  end
@@ -103,12 +122,6 @@ module Capistrano
103
122
  role.user
104
123
  end
105
124
 
106
- def cable_bind
107
- Array(fetch(:cable_bind)).collect do |bind|
108
- "bind '#{bind}'"
109
- end.join("\n")
110
- end
111
-
112
125
  def service_unit_type
113
126
  ## Jruby don't support notify
114
127
  return "simple" if RUBY_ENGINE == "jruby"
@@ -120,11 +133,7 @@ module Capistrano
120
133
  def puma_options
121
134
  options = []
122
135
  options << "--no-config"
123
- if fetch(:cable_ssl_certificate) && fetch(:cable_ssl_certificate_key)
124
- options << "--bind 'ssl://0.0.0.0:#{fetch(:cable_port)}?key=#{fetch(:cable_ssl_certificate_key)}&cert=#{fetch(:cable_ssl_certificate)}'"
125
- else
126
- options << "--port #{fetch(:cable_port)}"
127
- end
136
+ cable_binds.each { |bind| options << "--bind '#{bind}'" }
128
137
  options << "--environment #{fetch(:cable_env)}"
129
138
  options << "--pidfile #{fetch(:cable_pidfile)}" if fetch(:cable_pidfile)
130
139
  options << "--threads #{fetch(:cable_threads)}" if fetch(:cable_threads)
@@ -156,10 +165,11 @@ module Capistrano
156
165
  end
157
166
 
158
167
  def cable_binds
159
- Array(fetch(:cable_bind)).map do |m|
160
- etype, address = /(tcp|unix|ssl):\/{1,2}(.+)/.match(m).captures
161
- Bind.new(m, etype.to_sym, address)
162
- end
168
+ Array(fetch(:cable_bind)).map { |bind| Bind.new(bind) }
169
+ end
170
+
171
+ def cable_socket_dirs
172
+ cable_binds.select(&:unix?).map { |bind| File.dirname(bind.address) }.uniq
163
173
  end
164
174
  end
165
175
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capistrano
4
4
  module Cable
5
- VERSION = "0.1.2"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -3,8 +3,14 @@
3
3
  git_plugin = self
4
4
 
5
5
  namespace :cable do
6
+ desc "Check Cable configuration"
7
+ task :check do
8
+ git_plugin.check_removed_options!
9
+ end
10
+
6
11
  desc "Install Cable systemd service"
7
12
  task :install do
13
+ git_plugin.check_removed_options!
8
14
  on roles(fetch(:cable_role)) do |role|
9
15
  upload_compiled_template = lambda do |template_name, unit_filename|
10
16
  git_plugin.upload_template_cable template_name, "#{fetch(:tmp_dir)}/#{unit_filename}", role
@@ -17,6 +23,8 @@ namespace :cable do
17
23
  end
18
24
  end
19
25
 
26
+ git_plugin.cable_socket_dirs.each { |dir| execute :mkdir, "-p", dir }
27
+
20
28
  upload_compiled_template.call("cable.service", "#{fetch(:cable_service_unit_name)}.service")
21
29
 
22
30
  if fetch(:cable_enable_socket_service)
@@ -12,11 +12,12 @@
12
12
  [Unit]
13
13
  Description=Cable HTTP Server for <%= "#{fetch(:application)} (#{fetch(:stage)})" %>
14
14
  <%= "Requires=#{fetch(:cable_socket_unit_name)}" if fetch(:cable_enable_socket_service) %>
15
- After=syslog.target network.target
15
+ After=syslog.target network.target<%= " #{fetch(:cable_socket_unit_name)}" if fetch(:cable_enable_socket_service) %>
16
16
 
17
17
  [Service]
18
18
  Type=<%= service_unit_type %>
19
19
  WatchdogSec=30
20
+ <%="LimitNOFILE=#{fetch(:cable_limit_nofile)}" if fetch(:cable_limit_nofile) %>
20
21
  <%="User=#{cable_user(@role)}" if fetch(:cable_systemctl_user) == :system %>
21
22
  <%="PIDFile=#{fetch(:cable_pidfile)}" if fetch(:cable_pidfile) %>
22
23
  WorkingDirectory=<%= current_path %>
@@ -3,17 +3,19 @@ Description=Cable Puma HTTP Server Accept Sockets for <%= "#{fetch(:application)
3
3
 
4
4
  [Socket]
5
5
  <% cable_binds.each do |bind| -%>
6
- <%= "ListenStream=#{bind.local.address}" %>
6
+ ListenStream=<%= bind.address %>
7
7
  <% end -%>
8
8
 
9
9
  # Don't let systemd accept the request, wait for Cable to do that.
10
10
  # Systemd will start the cable service upon first request if it wasn't started.
11
11
  #
12
- # You might also want to set your Nginx upstream to have a fail_timeout large enough to accomodate your app's
13
- # startup time.
12
+ # Systemd keeps the socket open across restarts of the service: connections are
13
+ # queued in the backlog instead of being refused while Cable boots.
14
+ #
15
+ # You might also want to give the web server in front a connection timeout large
16
+ # enough to accommodate your app's startup time.
14
17
  Accept=no
15
- <%= "NoDelay=true" if fetch(:cable_systemctl_user) == :system %>
16
- ReusePort=true
18
+ <%= "NoDelay=true" unless cable_binds.all?(&:unix?) %>
17
19
  Backlog=1024
18
20
 
19
21
  SyslogIdentifier=<%= fetch(:cable_socket_unit_name) %>
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capistrano-cable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brice TEXIER
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-07-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: capistrano
@@ -51,7 +50,6 @@ metadata:
51
50
  homepage_uri: https://github.com/codeur/capistrano-cable#readme
52
51
  source_code_uri: https://github.com/codeur/capistrano-cable
53
52
  changelog_uri: https://github.com/codeur/capistrano-cable/releases
54
- post_install_message:
55
53
  rdoc_options: []
56
54
  require_paths:
57
55
  - lib
@@ -66,8 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
64
  - !ruby/object:Gem::Version
67
65
  version: '0'
68
66
  requirements: []
69
- rubygems_version: 3.3.26
70
- signing_key:
67
+ rubygems_version: 4.0.16
71
68
  specification_version: 4
72
69
  summary: ActionCable integration for Capistrano
73
70
  test_files: []