Skip to content

Écoute du serveur Cable sur une socket unix, via socket activation systemd - #1

Merged
burisu merged 3 commits into
mainfrom
feature/unix-socket-bind
Sep 15, 2026
Merged

burisu merged 3 commits into
mainfrom
feature/unix-socket-bind

Conversation

@burisu

@burisu burisu commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Problème

Le serveur Cable écoute sur un port TCP, monté en dur sur 0.0.0.0 : puma --no-config --port 28090. Deux conséquences :

  • le port est joignable depuis l'extérieur, alors que seul le serveur web en frontal a besoin d'y accéder ;
  • il faut réserver un port libre par app et par stage, sur des serveurs qui hébergent plusieurs apps (app4 en particulier).

La gem contenait déjà tout le décor pour faire autrement (Bind, cable_binds, cable.socket.erb, cable_enable_socket_service), hérité de capistrano-puma, mais ce chemin était mort : :cable_bind n'était jamais défini (ligne commentée) et puma_options l'ignorait complètement. L'unité .socket se serait rendue sans aucun ListenStream.

Le Puma principal de nos apps, lui, tourne déjà sur une socket unix en socket activation (shared/tmp/sockets/web.sock), donc le terrain est connu.

Ce qui change

:cable_bind devient le seul réglage d'écoute. Il accepte une chaîne de bind Puma, ou un tableau :

set :cable_bind, "unix:///home/myapp/public_html/shared/tmp/sockets/cable.sock" # défaut
set :cable_bind, "tcp://0.0.0.0:28090"
set :cable_bind, "ssl://0.0.0.0:28090?cert=/path/cert.pem&key=/path/key.pem"

Par défaut, le serveur écoute donc sur une socket unix dans shared/tmp/sockets. :cable_port, :cable_ssl_certificate et :cable_ssl_certificate_key disparaissent, ainsi que les deux branches port/SSL de puma_options : le SSL n'est plus un cas particulier, c'est juste un bind.

La socket activation systemd est activée par défaut. L'unité .socket garde l'écoute ouverte pendant le redémarrage du service : les connexions sont mises en attente dans le backlog au lieu d'être refusées, et le serveur démarre à la première requête s'il est arrêté. Les websockets tombent quand même à chaque redémarrage (le client JS reconnecte), mais plus de connexion refusée entre les deux.

Au passage, sur ce chemin qui n'était donc jamais exécuté :

  • ListenStream vaut maintenant l'adresse du bind telle quelle. L'ancien Bind#local réécrivait 0.0.0.0 en 127.0.0.1, ce qui empêchait justement Puma d'apparier la socket reçue (il compare [:unix, chemin] / [:tcp, hôte, port] avec ses propres binds) : il aurait ouvert sa propre socket à côté. Supprimé.
  • Le parsing des binds ne perd plus le / initial d'un chemin unix (l'ancienne regex \/{1,2} transformait unix:/tmp/c.sock en ListenStream=tmp/c.sock, chemin relatif donc unité invalide). Un bind unix relatif est maintenant refusé avec un message explicite.
  • ReusePort=true est retiré : avec la socket activation il n'y a qu'un seul listener, et l'option ne sert qu'à masquer un appariement raté. NoDelay=true n'est plus émis que pour les binds TCP (l'option est sans objet sur une socket unix) au lieu d'être conditionné au mode :system.
  • Le service attend son unité socket (After=, en plus du Requires= déjà là).
  • la doc et le commentaire de l'unité socket ne parlent plus d'un serveur web en particulier : rien dans la gem n'en dépend, seul le chemin de la socket compte.
  • require "erb" / require "stringio" manquants : le rendu des templates plantait dès que la gem était chargée hors d'un contexte qui les avait déjà requis (c'est ce qui a fait échouer les premiers tests de rendu).

Migration des apps

Le but est qu'une app n'ait rien à faire côté Capistrano :

  • le défaut est la socket unix, donc pas de config à écrire ;
  • les unités systemd sont réinstallées à chaque déploiement, juste avant le redémarrage (cable:install puis cable:smart_restart sur deploy:finished) : mettre la gem à jour et déployer suffit, plus de cap production cable:install manuel ni de dérive entre la gem et l'unité installée ;
  • le dossier de la socket est créé par cable:install ;
  • si une app définit encore :cable_port ou une des deux options SSL, le déploiement s'arrête sur deploy:starting, avant d'avoir rien envoyé, avec le message qui dit quoi écrire à la place — plutôt que de basculer silencieusement l'écoute ailleurs. cable:install refuse aussi de tourner, pour les installations qui chargent le plugin sans ses hooks.

Le seul geste manuel est côté serveur web, qui doit proxifier vers le chemin de la socket au lieu de l'hôte et du port. Cette conf est gérée hors du repo, donc à changer dans la même fenêtre que le déploiement. Si ce n'est pas possible, une app peut garder son port avec set :cable_bind, "tcp://0.0.0.0:28090" et passer à la socket plus tard : le reste du changement (socket activation, install automatique) s'applique quand même.

Tests

  • test/capistrano/test_bind.rb : parsing des trois schémas, chemin unix absolu conservé avec deux ou trois slashes, options de certificat retirées de l'adresse, binds invalides refusés.
  • test/capistrano/test_systemd.rb : bind par défaut, --bind sur chaque bind configuré (et plus de --port), dossiers de socket à créer, refus des options supprimées, ordre install puis smart_restart sur deploy:finished, et rendu des deux unités (ListenStream par bind, NoDelay seulement en TCP, Requires=/After= de l'unité socket).
  • test/test_helper.rb charge maintenant la gem et pas seulement sa version.
  • bundle exec rake (test + standard) est vert. La suite affiche un avertissement de require circulaire venant de capistrano lui-même, sans rapport avec ces changements.
  • Les deux unités rendues pour codeur ont été validées par systemd-analyze verify --user sur app3 (exit 0, aucun avertissement), sans rien installer ni redémarrer.

Rien n'a été déployé.

🤖 Generated with Claude Code

burisu and others added 3 commits September 15, 2026 17:50
Replace cable_port and the two SSL options with a single cable_bind
option, defaulting to a unix socket in the shared directory, and turn the
socket unit on by default so the listening socket survives a restart of
the server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing in the gem depends on a specific web server, only on the socket
path, so drop the nginx upstream example and the Nginx mention in the
socket unit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check only ran from cable:install, at the very end of the deploy.
Run it from deploy:starting too, so an app still setting cable_port or
one of the SSL options is stopped before anything is uploaded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@burisu
burisu marked this pull request as ready for review September 15, 2026 20:14
@burisu
burisu merged commit 41e05b4 into main Sep 15, 2026
2 checks passed
@burisu
burisu deleted the feature/unix-socket-bind branch September 15, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant