Skip to content

Commit af95760

Browse files
Fix worker SIGBUS on supervisor restart. (#5)
1 parent 30f8c40 commit af95760

File tree

3 files changed

+36
-0
lines changed

3 files changed

+36
-0
lines changed

lib/async/service/supervisor/utilization_monitor.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def initialize(path, size: IO::Buffer::PAGE_SIZE * 8, segment_size: 512, growth_
3838
@segment_size = segment_size
3939
@growth_factor = growth_factor
4040

41+
File.unlink(path) rescue nil
4142
@file = File.open(path, "w+b")
4243
@file.truncate(size)
4344
# Supervisor maps the file for reading worker data

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Unlink shared memory file before opening on supervisor restart, preventing SIGBUS when workers still have the file mapped.
6+
37
## v0.13.0
48

59
- Add `worker_count` to `UtilizationMonitor` aggregated metrics per service, indicating how many workers contributed to each service's metrics (useful for utilization denominator).

test/async/service/utilization_monitor.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,37 @@
264264
expect(monitor.status[:data]).to be == {}
265265
end
266266

267+
it "does not resize existing file when recreating the utilization monitor" do
268+
# When the supervisor restarts, it recreates the SegmentAllocator. Without unlink,
269+
# File.open(path, "w+b") truncates the existing file. With unlink, we remove the file
270+
# first so the new allocator gets a fresh file; any process with the old file mapped
271+
# keeps a valid mapping to the unlinked inode.
272+
allocator = Async::Service::Supervisor::UtilizationMonitor::SegmentAllocator.new(
273+
shm_path, size: file_size, segment_size: segment_size
274+
)
275+
276+
# Resize to make the file larger than initial:
277+
larger_size = file_size * 2
278+
allocator.resize(larger_size)
279+
280+
# Open the file and keep a handle; this simulates a worker that has it mapped:
281+
existing_file = File.open(shm_path, "rb")
282+
original_size = existing_file.size
283+
expect(original_size).to be == larger_size
284+
285+
allocator.close
286+
287+
# Simulate supervisor restart - recreates allocator at same path:
288+
Async::Service::Supervisor::UtilizationMonitor::SegmentAllocator.new(
289+
shm_path, size: file_size, segment_size: segment_size
290+
)
291+
292+
# Our handle still references the original inode; it should not have been resized:
293+
expect(existing_file.size).to be == original_size
294+
ensure
295+
existing_file&.close
296+
end
297+
267298
it "frees segments when workers are removed" do
268299
# Register first worker
269300
monitor.register(supervisor_controller)

0 commit comments

Comments
 (0)