async-container 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/development.yml +36 -0
  3. data/.travis.yml +3 -3
  4. data/Gemfile +0 -3
  5. data/README.md +76 -9
  6. data/examples/async.rb +21 -0
  7. data/examples/channel.rb +44 -0
  8. data/examples/channels/client.rb +103 -0
  9. data/examples/container.rb +1 -1
  10. data/examples/isolate.rb +35 -0
  11. data/examples/minimal.rb +93 -0
  12. data/examples/test.rb +50 -0
  13. data/{title.rb → examples/title.rb} +0 -0
  14. data/examples/udppipe.rb +34 -0
  15. data/lib/async/container/best.rb +1 -1
  16. data/lib/async/container/channel.rb +57 -0
  17. data/lib/async/container/controller.rb +112 -21
  18. data/lib/async/container/error.rb +10 -0
  19. data/lib/async/container/forked.rb +3 -65
  20. data/lib/async/container/generic.rb +179 -8
  21. data/lib/async/container/group.rb +98 -93
  22. data/lib/async/container/hybrid.rb +2 -3
  23. data/lib/async/container/keyed.rb +53 -0
  24. data/lib/async/container/notify.rb +41 -0
  25. data/lib/async/container/notify/client.rb +61 -0
  26. data/lib/async/container/notify/pipe.rb +115 -0
  27. data/lib/async/container/notify/server.rb +111 -0
  28. data/lib/async/container/notify/socket.rb +86 -0
  29. data/lib/async/container/process.rb +167 -0
  30. data/lib/async/container/thread.rb +182 -0
  31. data/lib/async/container/threaded.rb +4 -90
  32. data/lib/async/container/version.rb +1 -1
  33. data/spec/async/container/controller_spec.rb +40 -0
  34. data/spec/async/container/forked_spec.rb +3 -1
  35. data/spec/async/container/hybrid_spec.rb +4 -1
  36. data/spec/async/container/notify/notify.rb +18 -0
  37. data/spec/async/container/notify/pipe_spec.rb +46 -0
  38. data/spec/async/container/notify_spec.rb +54 -0
  39. data/spec/async/container/shared_examples.rb +18 -6
  40. data/spec/async/container/threaded_spec.rb +2 -0
  41. metadata +27 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f6640daaa86e57d05f5c7ec2a0d25d98b355edb0926d45ba1e2414adfdaa46e
4
- data.tar.gz: fd25c58be73c64bf64dec42ac24e84b9f5c66fdb26da9780246c877bd38056f5
3
+ metadata.gz: fd0aec5639eb9f64edc338624657d7e6442948b5e4f403acac9264707a5d2bfe
4
+ data.tar.gz: 3ef49efb43cb2cf4c4470ce9ed98426d18d41e009e19f11c5f10a718a30b80c2
5
5
  SHA512:
6
- metadata.gz: e6011d00efba48db1d17fedf7fe9823210c58840611a140710be8ce88a55e39b5421bebf666d97a4741f5c97c1f16758f6bc7598fd4205ffb0b2c975173e496f
7
- data.tar.gz: d0c020e874296fc31abdd6638d7113ddb0ff7e718e142c9c8c77b40ad7874e848a78b9e351e45f9439421d1629d58cb20fe086d3966d07345c3cb46fe9a517a3
6
+ metadata.gz: c4a15262693a7928b00ecf157f57d2333e9760387e7d723ce9679732d21eeb566b855835e593db486e912976e8cdf69ce1a67b4c5a1d9f3c54614de2df39b9d2
7
+ data.tar.gz: e2d4e51a734a853852cf3aae1119a8a01698b62e6be5791612c6e008f67518cf651b39be3b959d45f4ca42830fa2b7e7f2fb2bfb8be8b82696bfaaa6009d39e1
@@ -0,0 +1,36 @@
1
+ name: Development
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ test:
7
+ strategy:
8
+ matrix:
9
+ os:
10
+ - ubuntu
11
+ - macos
12
+
13
+ ruby:
14
+ - 2.4
15
+ - 2.5
16
+ - 2.6
17
+ - 2.7
18
+
19
+ include:
20
+ - os: 'ubuntu'
21
+ ruby: '2.6'
22
+ env: COVERAGE=PartialSummary,Coveralls
23
+
24
+ runs-on: ${{matrix.os}}-latest
25
+
26
+ steps:
27
+ - uses: actions/checkout@v1
28
+ - uses: actions/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{matrix.ruby}}
31
+ - name: Install dependencies
32
+ run: |
33
+ command -v bundler || gem install bundler
34
+ bundle install
35
+ - name: Run tests
36
+ run: ${{matrix.env}} bundle exec rspec
@@ -4,14 +4,14 @@ cache: bundler
4
4
 
5
5
  matrix:
6
6
  include:
7
- - rvm: 2.3
8
7
  - rvm: 2.4
9
8
  - rvm: 2.5
10
9
  - rvm: 2.6
11
- - rvm: 2.6
12
- os: osx
10
+ # - rvm: 2.6
11
+ # os: osx
13
12
  - rvm: 2.6
14
13
  env: COVERAGE=BriefSummary,Coveralls
14
+ - rvm: 2.7
15
15
  - rvm: truffleruby
16
16
  - rvm: jruby-head
17
17
  env: JRUBY_OPTS="--debug -X+O"
data/Gemfile CHANGED
@@ -14,7 +14,4 @@ end
14
14
  group :test do
15
15
  gem 'benchmark-ips'
16
16
  gem 'ruby-prof', platforms: :mri
17
-
18
- gem 'simplecov'
19
- gem 'coveralls', require: false
20
17
  end
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Provides containers which implement concurrency policy for high-level servers (and potentially clients).
4
4
 
5
- [![Build Status](https://secure.travis-ci.org/socketry/async-container.svg)](http://travis-ci.org/socketry/async-container)
5
+ [![Actions Status](https://github.com/socketry/async-container/workflows/Development/badge.svg)](https://github.com/socketry/async-container/actions?workflow=Development)
6
6
  [![Code Climate](https://codeclimate.com/github/socketry/async-container.svg)](https://codeclimate.com/github/socketry/async-container)
7
7
  [![Coverage Status](https://coveralls.io/repos/socketry/async-container/badge.svg)](https://coveralls.io/r/socketry/async-container)
8
8
 
@@ -24,20 +24,87 @@ Or install it yourself as:
24
24
 
25
25
  ## Usage
26
26
 
27
+ ### Container
28
+
29
+ A container represents a set of child processes (or threads) which are doing work for you.
30
+
27
31
  ```ruby
28
- container = Async::Container::Threaded.new
32
+ require 'async/container'
33
+
34
+ Async.logger.debug!
29
35
 
30
- container.run(count: 8)
31
- # 8 reactors will be spawned, in separate threads.
32
- Server.new.run(...)
36
+ container = Async::Container.new
37
+
38
+ container.async do |task|
39
+ task.logger.debug "Sleeping..."
40
+ task.sleep(1)
41
+ task.logger.debug "Waking up!"
33
42
  end
34
43
 
35
- container = Async::Container::Forked.new
44
+ Async.logger.debug "Waiting for container..."
45
+ container.wait
46
+ Async.logger.debug "Finished."
47
+ ```
48
+
49
+ ### Controller
36
50
 
37
- container.run(count: 8) do
38
- # 8 reactors will be spawned, in separate forked processes.
39
- Server.new.run(...)
51
+ The controller provides the life-cycle management for one or more containers of processes. It provides behaviour like starting, restarting, reloading and stopping. You can see some [example implementations in Falcon](https://github.com/socketry/falcon/blob/master/lib/falcon/controller/). If the process running the controller receives `SIGHUP` it will recreate the container gracefully.
52
+
53
+ ```ruby
54
+ require 'async/container'
55
+
56
+ Async.logger.debug!
57
+
58
+ class Controller < Async::Container::Controller
59
+ def setup(container)
60
+ container.async do |task|
61
+ while true
62
+ Async.logger.debug("Sleeping...")
63
+ task.sleep(1)
64
+ end
65
+ end
66
+ end
40
67
  end
68
+
69
+ controller = Controller.new
70
+
71
+ controller.run
72
+
73
+ # If you send SIGHUP to this process, it will recreate the container.
74
+ ```
75
+
76
+ ### Signal Handling
77
+
78
+ `SIGINT` is the interrupt signal. The terminal sends it to the foreground process when the user presses **ctrl-c**. The default behavior is to terminate the process, but it can be caught or ignored. The intention is to provide a mechanism for an orderly, graceful shutdown.
79
+
80
+ `SIGQUIT` is the dump core signal. The terminal sends it to the foreground process when the user presses **ctrl-\**. The default behavior is to terminate the process and dump core, but it can be caught or ignored. The intention is to provide a mechanism for the user to abort the process. You can look at `SIGINT` as "user-initiated happy termination" and `SIGQUIT` as "user-initiated unhappy termination."
81
+
82
+ `SIGTERM` is the termination signal. The default behavior is to terminate the process, but it also can be caught or ignored. The intention is to kill the process, gracefully or not, but to first allow it a chance to cleanup.
83
+
84
+ `SIGKILL` is the kill signal. The only behavior is to kill the process, immediately. As the process cannot catch the signal, it cannot cleanup, and thus this is a signal of last resort.
85
+
86
+ `SIGSTOP` is the pause signal. The only behavior is to pause the process; the signal cannot be caught or ignored. The shell uses pausing (and its counterpart, resuming via `SIGCONT`) to implement job control.
87
+
88
+ ### Integration
89
+
90
+ #### systemd
91
+
92
+ Install a template file into `/etc/systemd/system/`:
93
+
94
+ ```
95
+ # my-daemon.service
96
+ [Unit]
97
+ Description=My Daemon
98
+ AssertPathExists=/srv/
99
+
100
+ [Service]
101
+ Type=notify
102
+ WorkingDirectory=/srv/my-daemon
103
+ ExecStart=bundle exec my-daemon
104
+ Nice=5
105
+
106
+ [Install]
107
+ WantedBy=multi-user.target
41
108
  ```
42
109
 
43
110
  ## Contributing
@@ -0,0 +1,21 @@
1
+
2
+ require 'kernel/sync'
3
+
4
+ class Worker
5
+ def initialize(&block)
6
+
7
+ end
8
+ end
9
+
10
+ Sync do
11
+ count.times do
12
+ worker = Worker.new(&block)
13
+
14
+ status = worker.wait do |message|
15
+
16
+ end
17
+
18
+ status.success?
19
+ status.failed?
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+
2
+ require 'json'
3
+
4
+ class Channel
5
+ def initialize
6
+ @in, @out = IO.pipe
7
+ end
8
+
9
+ def after_fork
10
+ @out.close
11
+ end
12
+
13
+ def receive
14
+ if data = @in.gets
15
+ return JSON.parse(data, symbolize_names: true)
16
+ end
17
+ end
18
+
19
+ def send(**message)
20
+ data = JSON.dump(message)
21
+
22
+ @out.puts(data)
23
+ end
24
+ end
25
+
26
+ status = Channel.new
27
+
28
+ pid = fork do
29
+ status.send(ready: false, status: "Initializing...")
30
+
31
+ # exit(-1) # crash
32
+
33
+ status.send(ready: true, status: "Initialization Complete!")
34
+ end
35
+
36
+ status.after_fork
37
+
38
+ while message = status.receive
39
+ pp message
40
+ end
41
+
42
+ pid, status = Process.waitpid2(pid)
43
+
44
+ puts "Status: #{status}"
@@ -0,0 +1,103 @@
1
+
2
+ require 'msgpack'
3
+ require 'async/io'
4
+ require 'async/io/stream'
5
+ require 'async/container'
6
+
7
+ # class Bus
8
+ # def initialize
9
+ #
10
+ # end
11
+ #
12
+ # def << object
13
+ # return :object
14
+ # end
15
+ #
16
+ # def [] key
17
+ # return
18
+ # end
19
+ #
20
+ # class Proxy < BasicObject
21
+ # def initialize(bus, name)
22
+ # @bus = bus
23
+ # @name = name
24
+ # end
25
+ #
26
+ # def inspect
27
+ # "[Proxy #{method_missing(:inspect)}]"
28
+ # end
29
+ #
30
+ # def method_missing(*args, &block)
31
+ # @bus.invoke(@name, args, &block)
32
+ # end
33
+ #
34
+ # def respond_to?(*args)
35
+ # @bus.invoke(@name, ["respond_to?", *args])
36
+ # end
37
+ # end
38
+ #
39
+ # class Wrapper < MessagePack::Factory
40
+ # def initialize(bus)
41
+ # super()
42
+ #
43
+ # self.register_type(0x00, Object,
44
+ # packer: @bus.method(:<<),
45
+ # unpacker: @bus.method(:[])
46
+ # )
47
+ #
48
+ # self.register_type(0x01, Symbol)
49
+ # self.register_type(0x02, Exception,
50
+ # packer: ->(exception){Marshal.dump(exception)},
51
+ # unpacker: ->(data){Marshal.load(data)},
52
+ # )
53
+ #
54
+ # self.register_type(0x03, Class,
55
+ # packer: ->(klass){Marshal.dump(klass)},
56
+ # unpacker: ->(data){Marshal.load(data)},
57
+ # )
58
+ # end
59
+ # end
60
+ #
61
+ # class Channel
62
+ # def self.pipe
63
+ # input, output = Async::IO.pipe
64
+ #
65
+ #
66
+ # end
67
+ #
68
+ # def initialize(input, output)
69
+ # @input = input
70
+ # @output = output
71
+ # end
72
+ #
73
+ # def read
74
+ # @input.read
75
+ # end
76
+ #
77
+ # def write
78
+ # end
79
+ # end
80
+
81
+ container = Async::Container.new
82
+ input, output = Async::IO.pipe
83
+
84
+ container.async do |instance|
85
+ stream = Async::IO::Stream.new(input)
86
+ output.close
87
+
88
+ while message = stream.gets
89
+ puts "Hello World from #{instance}: #{message}"
90
+ end
91
+
92
+ puts "exiting"
93
+ end
94
+
95
+ stream = Async::IO::Stream.new(output)
96
+
97
+ 5.times do |i|
98
+ stream.puts "#{i}"
99
+ end
100
+
101
+ stream.close
102
+
103
+ container.wait
@@ -9,7 +9,7 @@ Async.logger.debug(self, "Starting up...")
9
9
 
10
10
  controller = Async::Container::Controller.new do |container|
11
11
  Async.logger.debug(self, "Setting up container...")
12
-
12
+
13
13
  container.run(count: 1, restart: true) do
14
14
  Async.logger.debug(self, "Child process started.")
15
15
 
@@ -0,0 +1,35 @@
1
+
2
+ # We define end of life-cycle in terms of "Interrupt" (SIGINT), "Terminate" (SIGTERM) and "Kill" (SIGKILL, does not invoke user code).
3
+ class Terminate < Interrupt
4
+ end
5
+
6
+ class Isolate
7
+ def initialize(&block)
8
+
9
+ end
10
+ end
11
+
12
+
13
+ parent = Isolate.new do |parent|
14
+ preload_user_code
15
+ server = bind_socket
16
+ children = 4.times.map do
17
+ Isolate.new do |worker|
18
+ app = load_user_application
19
+ worker.ready!
20
+ server.accept do |peer|
21
+ app.handle_request(peer)
22
+ end
23
+ end
24
+ end
25
+ while status = parent.wait
26
+ # Status is not just exit status of process but also can be `:ready` or something else.
27
+ end
28
+ end
29
+
30
+ # Similar to Process.wait(pid)
31
+ status = parent.wait
32
+ # Life cycle controls
33
+ parent.interrupt!
34
+ parent.terminate!
35
+ parent.kill!
@@ -0,0 +1,93 @@
1
+
2
+ class Threaded
3
+ def initialize(&block)
4
+ @channel = Channel.new
5
+ @thread = Thread.new(&block)
6
+
7
+ @waiter = Thread.new do
8
+ begin
9
+ @thread.join
10
+ rescue Exception => error
11
+ finished(error)
12
+ else
13
+ finished
14
+ end
15
+ end
16
+ end
17
+
18
+ attr :channel
19
+
20
+ def close
21
+ self.terminate!
22
+ self.wait
23
+ ensure
24
+ @channel.close
25
+ end
26
+
27
+ def interrupt!
28
+ @thread.raise(Interrupt)
29
+ end
30
+
31
+ def terminate!
32
+ @thread.raise(Terminate)
33
+ end
34
+
35
+ def wait
36
+ if @waiter
37
+ @waiter.join
38
+ @waiter = nil
39
+ end
40
+
41
+ return @status
42
+ end
43
+
44
+ protected
45
+
46
+ def finished(error = nil)
47
+ @status = Status.new(error)
48
+ @channel.out.close
49
+ end
50
+ end
51
+
52
+ class Forked
53
+ def initialize(&block)
54
+ @channel = Channel.new
55
+ @status = nil
56
+
57
+ @pid = Process.fork do
58
+ Signal.trap(:INT) {raise Interrupt}
59
+ Signal.trap(:INT) {raise Terminate}
60
+
61
+ @channel.in.close
62
+
63
+ yield
64
+ end
65
+
66
+ @channel.out.close
67
+ end
68
+
69
+ attr :channel
70
+
71
+ def close
72
+ self.terminate!
73
+ self.wait
74
+ ensure
75
+ @channel.close
76
+ end
77
+
78
+ def interrupt!
79
+ Process.kill(:INT, @pid)
80
+ end
81
+
82
+ def terminate!
83
+ Process.kill(:TERM, @pid)
84
+ end
85
+
86
+ def wait
87
+ unless @status
88
+ pid, @status = ::Process.wait(@pid)
89
+ end
90
+
91
+ return @status
92
+ end
93
+ end