Skip to content

Redesign signal handling (RFC #220)#4984

Draft
SeanTAllen wants to merge 6 commits intomainfrom
redesign-signal-handling
Draft

Redesign signal handling (RFC #220)#4984
SeanTAllen wants to merge 6 commits intomainfrom
redesign-signal-handling

Conversation

@SeanTAllen
Copy link
Member

@SeanTAllen SeanTAllen commented Mar 8, 2026

Replaces the single-slot-per-signal design with multi-subscriber support, adds capability security via SignalAuth, and validates signal numbers through constrained types to prevent registration of fatal or uncatchable signals.

Added:

  • SignalAuth capability primitive (derived from AmbientAuth) for signal operations
  • ValidSignal / MakeValidSignal constrained types with platform-specific signal whitelists
  • Multiple subscribers per signal (up to 16)
  • New signals example program demonstrating the redesigned API

Changed (breaking):

  • SignalHandler.create now requires SignalAuth and ValidSignal parameters
  • SignalRaise.apply now requires SignalAuth
  • ANSITerm.create now requires SignalAuth as its first parameter — ANSITerm internally creates a SignalHandler for SIGWINCH to detect terminal resizes, so it needs signal authority. Any program using ANSITerm (including Readline) must pass a SignalAuth.

Runtime changes across all three ASIO backends (epoll, kqueue, IOCP):

  • Atomic CAS-based synchronous subscriber registration prevents race conditions between subscribe and signal delivery
  • Subscriber arrays use lock-free slot allocation with spin-wait for concurrent first-subscriber setup
  • Unsubscribe remains async through the ASIO thread queue

This is a feasibility prototype for the RFC — it may end up being the final implementation.

Replaces the single-slot-per-signal design with multi-subscriber
support, adds capability security via SignalAuth, and validates signal
numbers through constrained types to prevent registration of fatal or
uncatchable signals.

Runtime changes (all three ASIO backends):
- Atomic CAS-based synchronous subscriber registration prevents race
  conditions between subscribe and signal delivery
- Subscriber arrays use lock-free slot allocation with spin-wait for
  concurrent first-subscriber setup
- Unsubscribe remains async through the ASIO thread queue
- Assert on subscriber overflow (MAX_SIGNAL_SUBSCRIBERS=16)

Stdlib changes:
- New SignalAuth capability primitive (derived from AmbientAuth)
- New ValidSignal/MakeValidSignal constrained types with platform-
  specific signal whitelists
- SignalHandler requires SignalAuth and ValidSignal
- SignalRaise requires SignalAuth (accepts raw U32 since raising fatal
  signals is legitimate)
- ANSITerm constructor takes SignalAuth parameter

This is a feasibility prototype for the RFC, not a final implementation.
@ponylang-main ponylang-main added the discuss during sync Should be discussed during an upcoming sync label Mar 8, 2026
Multi-type PR (Added + Changed) requires manual CHANGELOG and
next-release.md updates rather than individual release note files.
The double-raise and 30-second timeout were workarounds for running
tests concurrently. The Makefile runs stdlib tests with --sequential,
so a single raise and standard 10-second timeout are sufficient.
…upport

Three issues:

1. _is_usr2 logic was inverted — it called Sig.usr2() when
   scheduler_scaling_pthreads WAS set (triggering compile_error on
   macOS CI) instead of when it was NOT set.

2. CI runs debug tests under lldb which only passes SIGINT through
   (process handle SIGINT --pass true --stop false). Tests using
   SIGTERM/SIGURG/SIGALRM caused lldb to stop the process. Changed
   all tests to use SIGINT since sequential execution prevents
   interference.

3. Added Windows branch to _is_handleable — Windows signal() only
   supports SIGINT and SIGTERM as catchable signals.
Actor constructors run asynchronously in Pony. When the test creates
s1 and s2 then calls s1.raise(), s2's constructor may not have executed
yet — so s2 isn't in the subscriber array when the signal fires.

Having each handler raise its own signal guarantees that handler's
constructor has completed (same-actor message ordering) before the
raise executes, ensuring it's subscribed when the signal arrives.
The double-raise is a test technique, not an API workaround.
@SeanTAllen SeanTAllen marked this pull request as draft March 9, 2026 20:31
Comment on lines +74 to +77
match MakeValidSignal(Sig.winch())
| let sig: ValidSignal =>
SignalHandler(auth, recover _TermResizeNotify(this) end, sig)
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should never fail but in actual final version we'd want to have an "Unreachable" here.

@SeanTAllen
Copy link
Member Author

Open issue: subscriber overflow

The current implementation uses a fixed-size array of 16 subscriber slots per signal. In debug builds, exceeding the limit triggers an assert crash. In release builds, the subscriber is silently dropped — the handler never receives notifications.

Neither outcome is acceptable. This needs to be replaced with a dynamically growable structure. The challenge is that the subscriber array is accessed from signal context (epoll) or the ASIO thread (kqueue, IOCP) with lock-free atomics, so the growth mechanism needs to be safe in that context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

discuss during sync Should be discussed during an upcoming sync

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants