async-safe 0.3.0 → 0.3.2

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: 8d77ebea20acb12bfc6f9e7848a809a17d993df98604ec9b457433ddf211437d
4
- data.tar.gz: b7bdcdf3d68df5a5f385a252926319b6ffb2154b3bb431507d63fb3fe64f255e
3
+ metadata.gz: 3d1ccb933e5a9b488f7c33e02d547ca69463e4b7c94fadccb3550196f9f765d5
4
+ data.tar.gz: 1050434843c8f29403076ef9bf3aa0946122e9e62d68d6aea5334c17cd5a449c
5
5
  SHA512:
6
- metadata.gz: 9cc0287a91eb60e17f4a6047574a88bf813dbe39ddf345738d3aacadd8a171ed3540e2e7f1004c987c45a5abd3afdfe83407cec276f08ede0a612a76fd0509da
7
- data.tar.gz: ada0c1d9c123abacc5688a787229a0c578d2b3c59b7dbd7fe2c944e990a9eb2ffa3de4842e43b22b3eb0521683a8c1f7ccfd5221be35df9f6d36ee16e3e33914
6
+ metadata.gz: e0a5426f5a9157f304e8e4d679199d5db4da2d82aba4322c84c3b67b1d61fb64c98c6370260d09f7a8615b8b785bd3691167e7ec897b31ee7d123b75e9ac2bc4
7
+ data.tar.gz: 9d0101093bf612f7915083bf7e03bf2e1bc38d9cb3cae8dbde5e7ae0507926a4f47dba0b39592581fca00b1c556137f45fdb71ba9688d40d123e9cbf9232b921
checksums.yaml.gz.sig CHANGED
Binary file
@@ -25,7 +25,7 @@ Async::Safe.enable!
25
25
 
26
26
  When a violation is detected, an `Async::Safe::ViolationError` will be raised immediately with details about the object, method, and execution contexts involved.
27
27
 
28
- ### Single-Owner Model (Opt-In)
28
+ ### Single-Owner Model
29
29
 
30
30
  By default, all classes are assumed to be async-safe. To enable tracking for specific classes, mark them with `ASYNC_SAFE = false`:
31
31
 
@@ -181,6 +181,145 @@ $ bundle exec sus
181
181
 
182
182
  Any thread safety violations will cause your tests to fail immediately with a clear error message showing which object was accessed incorrectly and from which fibers.
183
183
 
184
+ ## Determining Async Safety
185
+
186
+ When deciding whether to mark a class or method with `ASYNC_SAFE = false`, consider these factors:
187
+
188
+ ### Async-Safe Patterns
189
+
190
+ **Immutable objects:**
191
+ ~~~ ruby
192
+ class ImmutableUser
193
+ def initialize(name, email)
194
+ @name = name.freeze
195
+ @email = email.freeze
196
+ freeze # Entire object is frozen
197
+ end
198
+
199
+ attr_reader :name, :email
200
+ end
201
+ ~~~
202
+
203
+ **Pure functions (no state modification):**
204
+ ~~~ ruby
205
+ class Calculator
206
+ def add(a, b)
207
+ a + b # No instance state, pure computation
208
+ end
209
+ end
210
+ ~~~
211
+
212
+ **Thread-safe synchronization:**
213
+ ~~~ ruby
214
+ class SafeQueue
215
+ ASYNC_SAFE = true # Explicitly marked
216
+
217
+ def initialize
218
+ @queue = Thread::Queue.new # Thread-safe internally
219
+ end
220
+
221
+ def push(item)
222
+ @queue.push(item) # Delegates to thread-safe queue
223
+ end
224
+ end
225
+ ~~~
226
+
227
+ ### Unsafe (Single-Owner) Patterns
228
+
229
+ **Mutable instance state:**
230
+ ~~~ ruby
231
+ class Counter
232
+ ASYNC_SAFE = false # Enable tracking
233
+
234
+ def initialize
235
+ @count = 0
236
+ end
237
+
238
+ def increment
239
+ @count += 1 # Reads and writes @count (race condition!)
240
+ end
241
+ end
242
+ ~~~
243
+
244
+ **Stateful iteration:**
245
+ ~~~ ruby
246
+ class Reader
247
+ ASYNC_SAFE = false # Enable tracking
248
+
249
+ def initialize(data)
250
+ @data = data
251
+ @index = 0
252
+ end
253
+
254
+ def read
255
+ value = @data[@index]
256
+ @index += 1 # Mutates state
257
+ value
258
+ end
259
+ end
260
+ ~~~
261
+
262
+ **Lazy initialization:**
263
+ ~~~ ruby
264
+ class DataLoader
265
+ ASYNC_SAFE = false # Enable tracking
266
+
267
+ def data
268
+ @data ||= load_data # Not atomic! (race condition)
269
+ end
270
+ end
271
+ ~~~
272
+
273
+ ### Mixed Safety
274
+
275
+ Use hash or array configuration for classes with both safe and unsafe methods:
276
+
277
+ ~~~ ruby
278
+ class MixedClass
279
+ ASYNC_SAFE = {
280
+ read_config: true, # Safe: only reads frozen data
281
+ update_state: false # Unsafe: modifies mutable state
282
+ }.freeze
283
+
284
+ def initialize
285
+ @config = {setting: "value"}.freeze
286
+ @state = {count: 0}
287
+ end
288
+
289
+ def read_config
290
+ @config[:setting] # Safe: frozen hash
291
+ end
292
+
293
+ def update_state
294
+ @state[:count] += 1 # Unsafe: mutates state
295
+ end
296
+ end
297
+ ~~~
298
+
299
+ ### Quick Checklist
300
+
301
+ Mark a method as unsafe (`ASYNC_SAFE = false`) if it:
302
+ - ❌ Modifies instance variables.
303
+ - ❌ Uses `||=` for lazy initialization.
304
+ - ❌ Iterates with mutable state (like `@index`).
305
+ - ❌ Reads then writes shared state.
306
+ - ❌ Accesses mutable collections without synchronization.
307
+
308
+ A method is likely safe if it:
309
+ - ✅ Only reads from frozen/immutable data.
310
+ - ✅ Has no instance state.
311
+ - ✅ Uses only local variables.
312
+ - ✅ Delegates to thread-safe primitives `Thread::Queue`, `Mutex`, etc.
313
+ - ✅ The object itself is frozen.
314
+
315
+ ### When in Doubt
316
+
317
+ If you're unsure whether a class is thread-safe:
318
+ 1. **Mark it as unsafe** (`ASYNC_SAFE = false`) - let the monitoring catch any issues.
319
+ 2. **Run your tests** with monitoring enabled.
320
+ 3. **If no violations occur** after thorough testing, it's likely safe.
321
+ 4. **Review the code** for the patterns above.
322
+
184
323
  ## How It Works
185
324
 
186
325
  1. **Default Assumption**: All objects follow a single-owner model (not thread-safe).
@@ -47,7 +47,7 @@ module Async
47
47
  end
48
48
 
49
49
  private def build_message
50
- "Thread safety violation detected! #{@target.inspect}##{@method} was accessed from #{@current.inspect} by #{@owner.inspect}."
50
+ "Thread safety violation detected!\n\tObject: #{@target.inspect}##{@method}\n\tOwner: #{@owner.inspect}\n\tAccessed by: #{@current.inspect}"
51
51
  end
52
52
  end
53
53
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  module Async
7
7
  module Safe
8
- VERSION = "0.3.0"
8
+ VERSION = "0.3.2"
9
9
  end
10
10
  end
11
11
 
data/readme.md CHANGED
@@ -22,6 +22,10 @@ Please see the [project documentation](https://socketry.github.io/async-safe/) f
22
22
 
23
23
  Please see the [project releases](https://socketry.github.io/async-safe/releases/index) for all releases.
24
24
 
25
+ ### v0.3.2
26
+
27
+ - Better error message.
28
+
25
29
  ### v0.3.0
26
30
 
27
31
  - Inverted default model: classes are async-safe by default, use `ASYNC_SAFE = false` to enable tracking.
@@ -30,7 +34,6 @@ Please see the [project releases](https://socketry.github.io/async-safe/releases
30
34
  - Added `Class#async_safe?(method)` method for querying safety.
31
35
  - Removed logger feature: always raises `ViolationError` exceptions.
32
36
  - Removed `Async::Safe::Concurrent` module: use `async_safe!` instead.
33
- - Removed `reset!` method: use `disable!` + `enable!` instead.
34
37
 
35
38
  ### v0.2.0
36
39
 
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.3.2
4
+
5
+ - Better error message.
6
+
3
7
  ## v0.3.0
4
8
 
5
9
  - Inverted default model: classes are async-safe by default, use `ASYNC_SAFE = false` to enable tracking.
@@ -8,7 +12,6 @@
8
12
  - Added `Class#async_safe?(method)` method for querying safety.
9
13
  - Removed logger feature: always raises `ViolationError` exceptions.
10
14
  - Removed `Async::Safe::Concurrent` module: use `async_safe!` instead.
11
- - Removed `reset!` method: use `disable!` + `enable!` instead.
12
15
 
13
16
  ## v0.2.0
14
17
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-safe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file