ask-app-server 0.4.11 → 0.4.13

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: 355decdbdc2e02ab22ec3b46c05dd1481342b68b5430a8a0fa0c57d827f2c564
4
- data.tar.gz: 3e53e334780abfb58fd6ad0173c47b6eee8c24e398610b0423e26fa538a92fcb
3
+ metadata.gz: 7108a931b0787f70885182225d0a1623f238d503b9c943ebaf0578bf4a7fb35f
4
+ data.tar.gz: a1cb98beb16710a0133d42eb31d4481215451978dd4a1a65e487da74e080e9eb
5
5
  SHA512:
6
- metadata.gz: ae55dc50c9c9fc8b75cbfe78c1f46204a6cf6a9e0b3b1653aaa79cfe5eee9d10f3ef537681ca0c14a7705e3ba6bc889f07def82159c9a583e1e8deb5c51e144c
7
- data.tar.gz: 4355631853f03a45dc014faff33967064a28c61e1265be8bb7827dbafcbea83aff1d4d0a0df4086eb59c9ab7187a8b242eb6fb2e9468db2bf35528c84f9379e9
6
+ metadata.gz: 020b6bc818e607773e47a0b53e63b7b57a8d100cc14310ffba6218df5637984b13a7b41f79794d1b2ed5e763cea2ac9fbcf4afc902d841ef82dcec716bbd86f4
7
+ data.tar.gz: 457b10ce1090f738709d0f328400035b41fabfda09bfc3ddb38ef7ae770bc1e954ddb07007f6ded2c5d22ad6b2e657e2a54b710e11d40eb5af74d931c3c9c335
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.13] - 2026-09-10
4
+
5
+ ### Fixed
6
+
7
+ - **A stopped host no longer deletes its successor's socket.** The
8
+ socket path is a fixed, shared name and hosts overlap: `#start`
9
+ unlinks a stale path, so a successor launched over a live host takes
10
+ the name, and the predecessor's later exit (idle timeout, crash,
11
+ manual kill) ran an unconditional `rm_f` that deleted the path the
12
+ successor was listening on. Every client after that got ENOENT while
13
+ the successor stayed alive and looked healthy. The server now records
14
+ the (device, inode) it bound and removes the file only if it is still
15
+ that one. Seen in the wild: a host orphaned two weeks earlier was
16
+ killed during a routine cleanup and silently broke the host that had
17
+ replaced it.
18
+
3
19
  ## [0.4.0] - 2026-08-11
4
20
 
5
21
  ### Added
@@ -47,20 +47,37 @@ module Ask
47
47
  FileUtils.mkdir_p(File.dirname(@socket_path))
48
48
  FileUtils.rm_f(@socket_path) # stale socket from a previous run
49
49
  @listener = UNIXServer.new(@socket_path)
50
+ # Remember which inode we bound. A later host on the same path
51
+ # unlinks ours and binds its own; without this, our #stop would
52
+ # delete the successor's socket (see #stop).
53
+ @bound_identity = identity_of(@socket_path)
50
54
  @acceptor = Thread.new { accept_loop }
51
55
  @pusher = Thread.new { pusher_loop }
52
56
  @logger.debug("Socket server listening on #{@socket_path}")
53
57
  self
54
58
  end
55
59
 
56
- # Stop accepting, close all connections, and remove the socket file.
60
+ # Stop accepting, close all connections, and remove the socket file
61
+ # — but only if it is still OURS.
62
+ #
63
+ # The socket path is a fixed, shared name, and hosts overlap: a
64
+ # predecessor starting up unlinks a stale path, and a successor
65
+ # started over a live one unlinks its predecessor's. The
66
+ # predecessor then exits (idle timeout, crash, manual kill) and an
67
+ # unconditional rm_f would delete the path the SUCCESSOR is
68
+ # listening on — every client after that gets ENOENT while the
69
+ # successor stays alive and looks healthy. Comparing the inode we
70
+ # bound against what is on disk now keeps a dying process from
71
+ # taking the live one down with it.
57
72
  def stop
58
73
  @running = false
59
74
  @acceptor&.kill rescue nil
60
75
  @pusher&.kill rescue nil
61
76
  @protocol.connections.each { |conn| @protocol.remove_connection(conn) }
62
77
  @listener&.close rescue nil
63
- FileUtils.rm_f(@socket_path) rescue nil
78
+ FileUtils.rm_f(@socket_path) if ours_on_disk?
79
+ rescue SystemCallError
80
+ nil
64
81
  end
65
82
 
66
83
  # Block until the acceptor stops (the process's main loop in
@@ -76,6 +93,21 @@ module Ask
76
93
 
77
94
  private
78
95
 
96
+ # (device, inode) of the socket at path, or nil when it is gone.
97
+ def identity_of(path)
98
+ stat = File.stat(path)
99
+ [stat.dev, stat.ino]
100
+ rescue SystemCallError
101
+ nil
102
+ end
103
+
104
+ # True when the path still points at the socket WE bound. False once
105
+ # a successor has replaced it (or it was removed altogether), which
106
+ # means the file is not ours to delete.
107
+ def ours_on_disk?
108
+ @bound_identity && identity_of(@socket_path) == @bound_identity
109
+ end
110
+
79
111
  def accept_loop
80
112
  while @running
81
113
  socket = @listener.accept
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module AppServer
5
- VERSION = "0.4.11"
5
+ VERSION = "0.4.13"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-app-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.11
4
+ version: 0.4.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto