async-actor 0.1.1 → 0.2.0
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +148 -0
- data/context/index.yaml +13 -0
- data/lib/async/actor/proxy.rb +15 -6
- data/lib/async/actor/version.rb +1 -1
- data/lib/async/actor.rb +7 -2
- data/readme.md +21 -3
- data/releases.md +15 -0
- data.tar.gz.sig +0 -0
- metadata +13 -15
- metadata.gz.sig +0 -0
- data/lib/async/actor/variable.rb +0 -63
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a0faec5a9475a5c7ac0d593c18e2707583043f74329fd07ccf107533877b2af
|
4
|
+
data.tar.gz: 5cefcda45e0e6acc25f8418d0340526b8af9849caf7f3e8bc4cc39f35560f353
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55c05936c7a06a08448128d8b98fd2d5258ccdb93303f635add67d3de3f41ec2cc7aa8047f071c04c6a08e831f7245c3fd037521baddaf20e9631e09df139732
|
7
|
+
data.tar.gz: 4573e8fff48e9508ae01fc6c65d9d3ea369291057a092db3ca32196694d799e9222b6399c0187e5c28247ebb537a1ed69faec0d9c13eada6645d08006aff36d7
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# Getting Started
|
2
|
+
|
3
|
+
This guide explains how to use `async-actor` for asynchronous programming.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add the gem to your project:
|
8
|
+
|
9
|
+
~~~ bash
|
10
|
+
$ bundle add async-actor
|
11
|
+
~~~
|
12
|
+
|
13
|
+
## Core Concepts
|
14
|
+
|
15
|
+
`async-actor` provides a simple actor model where each actor runs in its own dedicated thread. The {ruby Async::Actor::Proxy} class creates a proxy interface that wraps any object and executes its methods asynchronously in a separate thread, using an event loop. When you call methods on the proxy, they are queued and processed by the internal actor thread, allowing for concurrent execution while maintaining thread isolation.
|
16
|
+
|
17
|
+
### Lifecycle Separation
|
18
|
+
|
19
|
+
One of the key benefits of actors is **lifecycle separation**. An actor can run for the entire lifetime of your process, independent of your application's async contexts. This means you can have long-running actors that persist even while your frontend usage of Async may start and stop multiple times.
|
20
|
+
|
21
|
+
For example, you might have a logging actor or database connection pool that runs continuously, while your web server or other components restart their async contexts as needed. The actor maintains its own thread and state, providing a stable foundation for your application's infrastructure. A common use case is in test suites where you need to cache access to background connection pools for performance reasons - you can create an actor that manages database connections or external service clients, allowing you to reuse these expensive resources between tests while each test runs in its own isolated async context.
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Any existing object can be wrapped into an actor:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require "async/actor"
|
29
|
+
|
30
|
+
require "cgi"
|
31
|
+
require "net/http"
|
32
|
+
require "json"
|
33
|
+
|
34
|
+
class Wikipedia
|
35
|
+
def summary_url(title)
|
36
|
+
URI "https://en.wikipedia.org/api/rest_v1/page/summary/#{CGI.escape title}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def lookup(title)
|
40
|
+
JSON.parse(Net::HTTP.get(summary_url(title))).fetch("extract")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
wikipedia = Async::Actor.new(Wikipedia.new)
|
45
|
+
|
46
|
+
puts wikipedia.lookup("Ruby_(programming_language)")
|
47
|
+
```
|
48
|
+
|
49
|
+
The above code looks deceptively simple, however `wikipedia.lookup` actually sends a message to the actor using a message queue. The proxy creates a dedicated thread that runs an async event loop, and the actor processes the message within this thread. This allows the actor to handle multiple messages concurrently within its own thread while keeping it isolated from the main thread. When the result is ready, the actor thread notifies the caller with the result.
|
50
|
+
|
51
|
+
Be aware that as the actor is running in a separate thread, your code will need to be thread-safe, including arguments that you pass to the actor. Any block you provide will also be executed in the actor's thread.
|
52
|
+
|
53
|
+
## Return Value Control
|
54
|
+
|
55
|
+
The actor proxy provides flexible control over how method calls return values through the `return_value` parameter. This parameter accepts three options:
|
56
|
+
|
57
|
+
### `:wait` (Default)
|
58
|
+
|
59
|
+
By default, method calls block until the result is available and return the actual result:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
wikipedia = Async::Actor.new(Wikipedia.new)
|
63
|
+
|
64
|
+
# This blocks until the lookup completes and returns the extract
|
65
|
+
result = wikipedia.lookup("Ruby_(programming_language)")
|
66
|
+
puts result # Prints the Wikipedia extract
|
67
|
+
```
|
68
|
+
|
69
|
+
This is equivalent to:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
result = wikipedia.lookup("Ruby_(programming_language)", return_value: :wait)
|
73
|
+
```
|
74
|
+
|
75
|
+
### `:promise`
|
76
|
+
|
77
|
+
Returns an `Async::Promise` immediately, allowing you to wait for the result later:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
wikipedia = Async::Actor.new(Wikipedia.new)
|
81
|
+
|
82
|
+
# Returns immediately with a promise
|
83
|
+
promise = wikipedia.lookup("Ruby_(programming_language)", return_value: :promise)
|
84
|
+
|
85
|
+
# Do other work...
|
86
|
+
puts "Looking up information..."
|
87
|
+
|
88
|
+
# Wait for the result when needed
|
89
|
+
result = promise.wait
|
90
|
+
puts result
|
91
|
+
```
|
92
|
+
|
93
|
+
This is useful for fire-and-forget operations or when you want to start multiple operations concurrently:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# Start multiple lookups concurrently
|
97
|
+
ruby_promise = wikipedia.lookup("Ruby_(programming_language)", return_value: :promise)
|
98
|
+
python_promise = wikipedia.lookup("Python_(programming_language)", return_value: :promise)
|
99
|
+
java_promise = wikipedia.lookup("Java_(programming_language)", return_value: :promise)
|
100
|
+
|
101
|
+
# Wait for all results
|
102
|
+
puts "Ruby: #{ruby_promise.wait}"
|
103
|
+
puts "Python: #{python_promise.wait}"
|
104
|
+
puts "Java: #{java_promise.wait}"
|
105
|
+
```
|
106
|
+
|
107
|
+
### `:ignore`
|
108
|
+
|
109
|
+
Executes the method but discards the result, returning `nil` immediately:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
logger = Async::Actor.new(Logger.new)
|
113
|
+
|
114
|
+
# Fire-and-forget logging - returns nil immediately
|
115
|
+
logger.info("Application started", return_value: :ignore)
|
116
|
+
|
117
|
+
# Continue with other work without waiting
|
118
|
+
puts "Continuing execution..."
|
119
|
+
```
|
120
|
+
|
121
|
+
This is perfect for logging, notifications, or other side-effect operations where you don't need the return value.
|
122
|
+
|
123
|
+
## Error Handling
|
124
|
+
|
125
|
+
Errors from actor method calls are propagated to the caller:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
wikipedia = Async::Actor.new(Wikipedia.new)
|
129
|
+
|
130
|
+
begin
|
131
|
+
# This will raise if the network request fails
|
132
|
+
result = wikipedia.lookup("NonexistentPage")
|
133
|
+
rescue => error
|
134
|
+
puts "Lookup failed: #{error.message}"
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
With promises, errors are raised when you call `wait`:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
promise = wikipedia.lookup("NonexistentPage", return_value: :promise)
|
142
|
+
|
143
|
+
begin
|
144
|
+
result = promise.wait
|
145
|
+
rescue => error
|
146
|
+
puts "Lookup failed: #{error.message}"
|
147
|
+
end
|
148
|
+
```
|
data/context/index.yaml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
3
|
+
---
|
4
|
+
description: A multi-threaded actor implementation where each actor has it's own event
|
5
|
+
loop.
|
6
|
+
metadata:
|
7
|
+
documentation_uri: https://socketry.github.io/async-actor/
|
8
|
+
funding_uri: https://github.com/sponsors/ioquatix
|
9
|
+
source_code_uri: https://github.com/socketry/async-actor.git
|
10
|
+
files:
|
11
|
+
- path: getting-started.md
|
12
|
+
title: Getting Started
|
13
|
+
description: This guide explains how to use `async-actor` for asynchronous programming.
|
data/lib/async/actor/proxy.rb
CHANGED
@@ -3,25 +3,32 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2023, by Samuel Williams.
|
5
5
|
|
6
|
-
require
|
7
|
-
|
8
|
-
require_relative 'variable'
|
6
|
+
require "async"
|
9
7
|
|
10
8
|
module Async
|
11
9
|
module Actor
|
10
|
+
# Represents an asynchronous proxy that wraps an object and executes its methods in a separate thread.
|
12
11
|
class Proxy < BasicObject
|
12
|
+
# Handles cleanup of proxy resources when the proxy is garbage collected.
|
13
13
|
class Finalizer
|
14
|
+
# Initialize a new finalizer.
|
15
|
+
# @parameter queue [Thread::Queue] The message queue to close.
|
16
|
+
# @parameter thread [Thread] The worker thread to join.
|
14
17
|
def initialize(queue, thread)
|
15
18
|
@queue = queue
|
16
19
|
@thread = thread
|
17
20
|
end
|
18
21
|
|
22
|
+
# Clean up proxy resources by closing the queue and joining the thread.
|
23
|
+
# @parameter id [Object] The object id (unused but required by ObjectSpace.define_finalizer).
|
19
24
|
def call(id)
|
20
25
|
@queue.close
|
21
26
|
@thread.join
|
22
27
|
end
|
23
28
|
end
|
24
29
|
|
30
|
+
# Initialize a new proxy for the target object.
|
31
|
+
# @parameter target [Object] The object to wrap with asynchronous method execution.
|
25
32
|
def initialize(target)
|
26
33
|
@target = target
|
27
34
|
|
@@ -35,7 +42,7 @@ module Async
|
|
35
42
|
# @parameter return_value [Symbol] One of :ignore, :promise or :wait.
|
36
43
|
def method_missing(*arguments, return_value: :wait, **options, &block)
|
37
44
|
unless return_value == :ignore
|
38
|
-
result =
|
45
|
+
result = Promise.new
|
39
46
|
end
|
40
47
|
|
41
48
|
@queue.push([arguments, options, block, result])
|
@@ -43,7 +50,7 @@ module Async
|
|
43
50
|
if return_value == :promise
|
44
51
|
return result
|
45
52
|
else
|
46
|
-
return result&.
|
53
|
+
return result&.wait
|
47
54
|
end
|
48
55
|
end
|
49
56
|
|
@@ -55,7 +62,9 @@ module Async
|
|
55
62
|
while operation = @queue.pop
|
56
63
|
task.async do
|
57
64
|
arguments, options, block, result = operation
|
58
|
-
|
65
|
+
|
66
|
+
# Fulfill the promise with the result of the method call:
|
67
|
+
Promise.fulfill(result) do
|
59
68
|
@target.public_send(*arguments, **options, &block)
|
60
69
|
end
|
61
70
|
end
|
data/lib/async/actor/version.rb
CHANGED
data/lib/async/actor.rb
CHANGED
@@ -3,11 +3,16 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2023, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
6
|
+
require_relative "actor/version"
|
7
|
+
require_relative "actor/proxy"
|
8
8
|
|
9
|
+
# @namespace
|
9
10
|
module Async
|
11
|
+
# @namespace
|
10
12
|
module Actor
|
13
|
+
# Create a new actor proxy for the given instance.
|
14
|
+
# @parameter instance [Object] The target object to wrap in an actor proxy.
|
15
|
+
# @returns [Proxy] A new proxy that executes methods asynchronously.
|
11
16
|
def self.new(instance)
|
12
17
|
Proxy.new(instance)
|
13
18
|
end
|
data/readme.md
CHANGED
@@ -16,6 +16,24 @@ Please see the [project documentation](https://socketry.github.io/async-actor/)
|
|
16
16
|
|
17
17
|
- [Getting Started](https://socketry.github.io/async-actor/guides/getting-started/index) - This guide explains how to use `async-actor` for asynchronous programming.
|
18
18
|
|
19
|
+
## Releases
|
20
|
+
|
21
|
+
Please see the [project releases](https://socketry.github.io/async-actor/releases/index) for all releases.
|
22
|
+
|
23
|
+
### v0.2.0
|
24
|
+
|
25
|
+
### v0.1.1
|
26
|
+
|
27
|
+
- Fix dependency on async gem to use `>= 1` instead of `~> 1` for better compatibility.
|
28
|
+
- Update guide links in documentation.
|
29
|
+
|
30
|
+
### v0.1.0
|
31
|
+
|
32
|
+
- Initial release of async-actor gem.
|
33
|
+
- Core actor model implementation with `Async::Actor` class.
|
34
|
+
- Proxy class for safe cross-actor communication.
|
35
|
+
- Variable class for thread-safe value storage and updates.
|
36
|
+
|
19
37
|
## Contributing
|
20
38
|
|
21
39
|
We welcome contributions to this project.
|
@@ -28,8 +46,8 @@ We welcome contributions to this project.
|
|
28
46
|
|
29
47
|
### Developer Certificate of Origin
|
30
48
|
|
31
|
-
|
49
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
32
50
|
|
33
|
-
###
|
51
|
+
### Community Guidelines
|
34
52
|
|
35
|
-
This project is
|
53
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
data/releases.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Releases
|
2
|
+
|
3
|
+
## v0.2.0
|
4
|
+
|
5
|
+
## v0.1.1
|
6
|
+
|
7
|
+
- Fix dependency on async gem to use `>= 1` instead of `~> 1` for better compatibility.
|
8
|
+
- Update guide links in documentation.
|
9
|
+
|
10
|
+
## v0.1.0
|
11
|
+
|
12
|
+
- Initial release of async-actor gem.
|
13
|
+
- Core actor model implementation with `Async::Actor` class.
|
14
|
+
- Proxy class for safe cross-actor communication.
|
15
|
+
- Variable class for thread-safe value storage and updates.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-actor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain:
|
11
10
|
- |
|
@@ -37,41 +36,41 @@ cert_chain:
|
|
37
36
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
38
37
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
39
38
|
-----END CERTIFICATE-----
|
40
|
-
date:
|
39
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
41
40
|
dependencies:
|
42
41
|
- !ruby/object:Gem::Dependency
|
43
42
|
name: async
|
44
43
|
requirement: !ruby/object:Gem::Requirement
|
45
44
|
requirements:
|
46
|
-
- - "
|
45
|
+
- - "~>"
|
47
46
|
- !ruby/object:Gem::Version
|
48
|
-
version: '
|
47
|
+
version: '2.33'
|
49
48
|
type: :runtime
|
50
49
|
prerelease: false
|
51
50
|
version_requirements: !ruby/object:Gem::Requirement
|
52
51
|
requirements:
|
53
|
-
- - "
|
52
|
+
- - "~>"
|
54
53
|
- !ruby/object:Gem::Version
|
55
|
-
version: '
|
56
|
-
description:
|
57
|
-
email:
|
54
|
+
version: '2.33'
|
58
55
|
executables: []
|
59
56
|
extensions: []
|
60
57
|
extra_rdoc_files: []
|
61
58
|
files:
|
59
|
+
- context/getting-started.md
|
60
|
+
- context/index.yaml
|
62
61
|
- lib/async/actor.rb
|
63
62
|
- lib/async/actor/proxy.rb
|
64
|
-
- lib/async/actor/variable.rb
|
65
63
|
- lib/async/actor/version.rb
|
66
64
|
- license.md
|
67
65
|
- readme.md
|
68
|
-
|
66
|
+
- releases.md
|
67
|
+
homepage: https://github.com/socketry/async-actor
|
69
68
|
licenses:
|
70
69
|
- MIT
|
71
70
|
metadata:
|
72
71
|
documentation_uri: https://socketry.github.io/async-actor/
|
73
72
|
funding_uri: https://github.com/sponsors/ioquatix
|
74
|
-
|
73
|
+
source_code_uri: https://github.com/socketry/async-actor.git
|
75
74
|
rdoc_options: []
|
76
75
|
require_paths:
|
77
76
|
- lib
|
@@ -79,15 +78,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
78
|
requirements:
|
80
79
|
- - ">="
|
81
80
|
- !ruby/object:Gem::Version
|
82
|
-
version: '3.
|
81
|
+
version: '3.2'
|
83
82
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
83
|
requirements:
|
85
84
|
- - ">="
|
86
85
|
- !ruby/object:Gem::Version
|
87
86
|
version: '0'
|
88
87
|
requirements: []
|
89
|
-
rubygems_version: 3.
|
90
|
-
signing_key:
|
88
|
+
rubygems_version: 3.6.9
|
91
89
|
specification_version: 4
|
92
90
|
summary: A multi-threaded actor implementation where each actor has it's own event
|
93
91
|
loop.
|
metadata.gz.sig
CHANGED
Binary file
|
data/lib/async/actor/variable.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Samuel Williams.
|
5
|
-
|
6
|
-
module Async
|
7
|
-
module Actor
|
8
|
-
class Variable
|
9
|
-
def self.fulfill(variable)
|
10
|
-
variable.set(yield)
|
11
|
-
variable = nil
|
12
|
-
rescue => error
|
13
|
-
variable&.fail(error)
|
14
|
-
variable = nil
|
15
|
-
ensure
|
16
|
-
# throw, etc:
|
17
|
-
variable&.fail(RuntimeError.new("Invalid flow control!"))
|
18
|
-
end
|
19
|
-
|
20
|
-
def initialize
|
21
|
-
@set = nil
|
22
|
-
@value = nil
|
23
|
-
|
24
|
-
@guard = Thread::Mutex.new
|
25
|
-
@condition = Thread::ConditionVariable.new
|
26
|
-
end
|
27
|
-
|
28
|
-
def set(value)
|
29
|
-
@guard.synchronize do
|
30
|
-
raise "Variable already set!" unless @set.nil?
|
31
|
-
|
32
|
-
@set = true
|
33
|
-
@value = value
|
34
|
-
@condition.broadcast
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def fail(error)
|
39
|
-
@guard.synchronize do
|
40
|
-
raise "Variable already set!" unless @set.nil?
|
41
|
-
|
42
|
-
@set = false
|
43
|
-
@error = error
|
44
|
-
@condition.broadcast
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def get
|
49
|
-
@guard.synchronize do
|
50
|
-
while @set.nil?
|
51
|
-
@condition.wait(@guard)
|
52
|
-
end
|
53
|
-
|
54
|
-
if @set
|
55
|
-
return @value
|
56
|
-
else
|
57
|
-
raise @error
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|