Skip to content

Commit c03096c

Browse files
Don't default to unsafe...
1 parent 89609b2 commit c03096c

File tree

10 files changed

+175
-260
lines changed

10 files changed

+175
-260
lines changed

guides/getting-started/readme.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ Async::Safe.enable!
2525

2626
When a violation is detected, an `Async::Safe::ViolationError` will be raised immediately with details about the object, method, and execution contexts involved.
2727

28-
### Single-Owner Model
28+
### Single-Owner Model (Opt-In)
2929

30-
By default, all objects are assumed to follow a **single-owner model** - they should only be accessed from one fiber/thread at a time:
30+
By default, all classes are assumed to be async-safe. To enable tracking for specific classes, mark them with `ASYNC_SAFE = false`:
3131

3232
~~~ ruby
3333
class MyBody
34+
ASYNC_SAFE = false # Enable tracking for this class
35+
3436
def initialize(chunks)
3537
@chunks = chunks
3638
@index = 0
@@ -90,15 +92,29 @@ class MyQueue
9092
end
9193
~~~
9294

93-
### Marking Async-Safe Methods
95+
Or use a hash for per-method configuration:
96+
97+
~~~ ruby
98+
class MixedClass
99+
ASYNC_SAFE = {
100+
read: true, # This method is async-safe
101+
write: false # This method is NOT async-safe
102+
}.freeze
103+
104+
# ... implementation
105+
end
106+
~~~
107+
108+
### Marking Methods with Hash
94109

95-
Mark specific methods as async-safe:
110+
Use a hash to specify which methods are async-safe:
96111

97112
~~~ ruby
98113
class MixedSafety
99-
include Async::Safe
100-
101-
async_safe :safe_read
114+
ASYNC_SAFE = {
115+
safe_read: true, # This method is async-safe
116+
increment: false # This method is NOT async-safe
117+
}.freeze
102118

103119
def initialize(data)
104120
@data = data
@@ -110,7 +126,7 @@ class MixedSafety
110126
end
111127

112128
def increment
113-
@count += 1 # Not async-safe
129+
@count += 1 # Not async-safe - will be tracked
114130
end
115131
end
116132

@@ -122,6 +138,17 @@ Fiber.schedule do
122138
end
123139
~~~
124140

141+
Or use an array to list async-safe methods:
142+
143+
~~~ ruby
144+
class MyClass
145+
ASYNC_SAFE = [:read, :inspect].freeze
146+
147+
# read and inspect are async-safe
148+
# all other methods will be tracked
149+
end
150+
~~~
151+
125152
### Transferring Ownership
126153

127154
Explicitly transfer ownership between fibers:

lib/async/safe.rb

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,12 @@
1212
module Async
1313
# Provides runtime thread safety monitoring for concurrent Ruby code.
1414
#
15-
# By default, all objects follow a **single-owner model** - they should only be accessed
16-
# from one fiber/thread at a time. Objects or methods can be explicitly marked as
17-
# async-safe to allow concurrent access.
15+
# By default, all classes are assumed to be async-safe. Classes that follow a
16+
# **single-owner model** should be explicitly marked with `ASYNC_SAFE = false` to
17+
# enable tracking and violation detection.
1818
#
1919
# Enable monitoring in your test suite to catch concurrency bugs early.
2020
module Safe
21-
# Include this module to mark specific methods as async-safe
22-
def self.included(base)
23-
base.extend(ClassMethods)
24-
end
25-
26-
# Class methods for marking async-safe methods
27-
module ClassMethods
28-
# Mark one or more methods as async-safe.
29-
#
30-
# @parameter method_names [Array(Symbol)] The methods to mark as async-safe.
31-
def async_safe(*method_names)
32-
@async_safe_methods ||= Set.new
33-
@async_safe_methods.merge(method_names)
34-
end
35-
36-
# Check if a method is async-safe.
37-
#
38-
# Overrides the default implementation from `Class` to also check method-level safety.
39-
#
40-
# @parameter method [Symbol | Nil] The method name to check, or nil to check if the entire class is async-safe.
41-
# @returns [Boolean] Whether the method or class is async-safe.
42-
def async_safe?(method = nil)
43-
# Check if entire class is marked async-safe:
44-
return true if super
45-
46-
# Check if specific method is marked async-safe:
47-
if method
48-
return @async_safe_methods&.include?(method)
49-
end
50-
51-
# Default to false if no method is specified and the class is not async safe:
52-
return false
53-
end
54-
end
55-
5621
class << self
5722
# @attribute [Monitor] The global monitoring instance.
5823
attr_reader :monitor
@@ -61,14 +26,8 @@ class << self
6126
#
6227
# This activates a TracePoint that tracks object access across fibers and threads.
6328
# There is no performance overhead when monitoring is disabled.
64-
#
65-
# @parameter logger [Object | Nil] Optional logger to use for violations instead of raising exceptions.
66-
def enable!(logger: nil)
67-
if @monitor
68-
raise "Async::Safe is already enabled!"
69-
end
70-
71-
@monitor = Monitor.new(logger: logger)
29+
def enable!
30+
@monitor ||= Monitor.new
7231
@monitor.enable!
7332
end
7433

lib/async/safe/builtins.rb

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,42 @@
88
# Note: Immutable values (nil, true, false, integers, symbols, etc.) are already
99
# handled by the frozen? check in the monitor and don't need to be listed here.
1010

11-
# Thread synchronization primitives:
12-
Thread::ASYNC_SAFE = true
13-
Thread::Queue::ASYNC_SAFE = true
14-
Thread::SizedQueue::ASYNC_SAFE = true
15-
Thread::Mutex::ASYNC_SAFE = true
16-
Thread::ConditionVariable::ASYNC_SAFE = true
17-
18-
# Fibers are async-safe:
19-
Fiber::ASYNC_SAFE = true
20-
21-
# ObjectSpace::WeakMap is async-safe:
22-
ObjectSpace::WeakMap::ASYNC_SAFE = true
23-
2411
module Async
2512
module Safe
13+
# Automatically transfers ownership of objects when they are removed from a Thread::Queue.
14+
#
15+
# When included in Thread::Queue or Thread::SizedQueue, this module wraps pop/deq/shift
16+
# methods to automatically transfer ownership of the dequeued object to the fiber that
17+
# dequeues it.
2618
module TransferableThreadQueue
19+
# Pop an object from the queue and transfer ownership to the current fiber.
20+
#
21+
# @parameter arguments [Array] Arguments passed to the original pop method.
22+
# @returns [Object] The dequeued object with transferred ownership.
2723
def pop(...)
2824
object = super(...)
2925
Async::Safe.transfer(object)
3026
object
3127
end
3228

29+
# Dequeue an object from the queue and transfer ownership to the current fiber.
30+
#
31+
# Alias for {#pop}.
32+
#
33+
# @parameter arguments [Array] Arguments passed to the original deq method.
34+
# @returns [Object] The dequeued object with transferred ownership.
3335
def deq(...)
3436
object = super(...)
3537
Async::Safe.transfer(object)
3638
object
3739
end
3840

41+
# Shift an object from the queue and transfer ownership to the current fiber.
42+
#
43+
# Alias for {#pop}.
44+
#
45+
# @parameter arguments [Array] Arguments passed to the original shift method.
46+
# @returns [Object] The dequeued object with transferred ownership.
3947
def shift(...)
4048
object = super(...)
4149
Async::Safe.transfer(object)

lib/async/safe/class.rb

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,40 @@
77
class Class
88
# Check if this class or a specific method is async-safe.
99
#
10+
# The `ASYNC_SAFE` constant can be:
11+
# - `true` - entire class is async-safe.
12+
# - `false` - entire class is NOT async-safe (single-owner).
13+
# - `{method_name: true/false}` - per-method configuration.
14+
# - `[method_name1, method_name2]` - per-method configuration.
15+
#
1016
# @parameter method [Symbol | Nil] The method name to check, or nil to check if the entire class is async-safe.
11-
# @returns [Boolean] Whether the class or method is async-safe.
17+
# @returns [Boolean] Whether the class or method is async-safe. Defaults to true if not specified.
1218
def async_safe?(method = nil)
13-
# Check if entire class is marked async-safe via constant:
14-
if const_defined?(:ASYNC_SAFE, false) && const_get(:ASYNC_SAFE)
15-
return true
19+
if const_defined?(:ASYNC_SAFE)
20+
async_safe = const_get(:ASYNC_SAFE)
21+
22+
case async_safe
23+
when Hash
24+
if method
25+
async_safe = async_safe.fetch(method, false)
26+
else
27+
# In general, some methods may not be safe:
28+
async_safe = false
29+
end
30+
when Array
31+
if method
32+
async_safe = async_safe.include?(method)
33+
else
34+
# In general, some methods may not be safe:
35+
async_safe = false
36+
end
37+
end
38+
39+
return async_safe
1640
end
1741

18-
false
42+
# Default to true:
43+
return true
1944
end
2045

2146
# Mark the class as async-safe or not.

lib/async/safe/monitor.rb

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,10 @@ class Monitor
6464
ASYNC_SAFE = true
6565

6666
# Initialize a new monitor instance.
67-
#
68-
# @parameter logger [Object | Nil] Optional logger to use for violations instead of raising exceptions.
69-
def initialize(logger: nil)
67+
def initialize
7068
@owners = ObjectSpace::WeakMap.new
7169
@mutex = Thread::Mutex.new
7270
@trace_point = nil
73-
@logger = logger
7471
end
7572

7673
attr :owners
@@ -131,11 +128,12 @@ def check_access(trace_point)
131128
if owner = @owners[object]
132129
# Violation if accessed from different fiber:
133130
if owner != current
134-
if @logger
135-
@logger.warn(self, "Async::Safe violation detected!", klass: klass, method: method, owner: owner, current: current, backtrace: caller_locations(3..))
136-
else
137-
raise ViolationError.new(target: object, method: method, owner: owner, current: current)
138-
end
131+
raise ViolationError.new(
132+
target: object,
133+
method: method,
134+
owner: owner,
135+
current: current,
136+
)
139137
end
140138
else
141139
# First access - record owner:

releases.md

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

3+
## Unreleased
4+
5+
- Inverted default model: classes are async-safe by default, use `ASYNC_SAFE = false` to enable tracking.
6+
- Added flexible `ASYNC_SAFE` constant support: boolean, hash, or array configurations.
7+
- Added `Class#async_safe!` method for marking classes.
8+
- Added `Class#async_safe?(method)` method for querying safety.
9+
- Removed logger feature: always raises `ViolationError` exceptions.
10+
- Removed `Async::Safe::Concurrent` module: use `async_safe!` instead.
11+
- Removed `reset!` method: use `disable!` + `enable!` instead.
12+
313
## v0.2.0
414

515
- `Thread::Queue` transfers ownership of objects popped from it.

0 commit comments

Comments
 (0)