async-service 0.19.1 → 0.20.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c0c2a2c76b02cc502a69553db8a737d22f23ed5186b46ac35f45e6f9d5ed497
4
- data.tar.gz: 4ca22e689898e5eabb150fefcab2025c248e4a0fae819bf6a7ff281de7249cf1
3
+ metadata.gz: dbf0b6499a4433f09585491d1097e280a739775f24557f57ea89a5f7e1c345f1
4
+ data.tar.gz: 7ffe1c3251852c4a57b6ad70f0e9d89f461bb272eef48cb9556f17e21369c6a6
5
5
  SHA512:
6
- metadata.gz: 1d1213fbae7c10e69b300fbfeec88878987654c862ac64969a72fa2b429ef96afc3426c5f82f8395b26963a9430172b8bdff9a25270bdfb0d0e159ed762cd002
7
- data.tar.gz: 42fec2e5500bcf14f06f35f149c566963404ceb8e9a155d256ebc8a76127a8a4adaee3f39452ff29e42aa8c357ec3305455cf4dc04de674973d499d9746037b4
6
+ metadata.gz: 477ed5f425e11f46b91da568ab84af8c32b74b873ed0909579cbad67b787a2c9230ae9aea871ae42fcb413437b5879b2ae36673c7c1a8a1117f27f759c2494cb
7
+ data.tar.gz: 83ec254a0835ea322ffdad5f41cfbf76ab322f739db150ab97ddecf8503b2f08408025e06613e552270f18dc9b9e3d5c934f62e7a0020e121fc55e0c13a45fef
checksums.yaml.gz.sig CHANGED
Binary file
data/context/index.yaml CHANGED
@@ -10,6 +10,10 @@ files:
10
10
  title: Getting Started
11
11
  description: This guide explains how to get started with `async-service` to create
12
12
  and run services in Ruby.
13
+ - path: policies.md
14
+ title: Container Policies
15
+ description: This guide explains how to configure container policies for your services
16
+ and understand the default failure handling behavior.
13
17
  - path: service-architecture.md
14
18
  title: Service Architecture
15
19
  description: This guide explains the key architectural components of `async-service`
@@ -0,0 +1,87 @@
1
+ # Container Policies
2
+
3
+ This guide explains how to configure container policies for your services and understand the default failure handling behavior.
4
+
5
+ ## Default Failure Handling
6
+
7
+ All services use {ruby Async::Service::Policy::DEFAULT} which monitors failure rates and stops the container when failures exceed a threshold.
8
+
9
+ **Default threshold:** 6 failures in 60 seconds (0.1 failures per second).
10
+
11
+ This means:
12
+ - Services can tolerate occasional failures and transient issues.
13
+ - More than 6 failures in any 60-second window stops the container.
14
+ - Prevents services from restart-looping indefinitely when fundamentally broken.
15
+
16
+ This fail-fast behavior is appropriate for orchestrated environments (Kubernetes, systemd) where the orchestrator will restart the entire service.
17
+
18
+ ### Why This Default?
19
+
20
+ Without failure monitoring, a broken service with `restart: true` would restart indefinitely, wasting resources. The default policy:
21
+
22
+ - **Catches problems quickly**: Broken services stop within 10-20 seconds.
23
+ - **Prevents resource waste**: Doesn't keep trying to start services that will never succeed.
24
+ - **Enables orchestrator recovery**: Systemd/Kubernetes can restart the whole process with a clean state.
25
+ - **Detects environmental issues**: Bad hardware, corrupted pre-fork state, or system-level problems can't be fixed by restarting children - the entire service needs to be restarted (potentially on different hardware).
26
+ - **Signals clear failure**: Exit code indicates the service couldn't maintain healthy operation.
27
+
28
+ ## Configuring Policies
29
+
30
+ Use `container_policy` in your service configuration to customize failure handling:
31
+
32
+ ``` ruby
33
+ # config/service.rb
34
+
35
+ # More lenient: allow 5 failures per minute:
36
+ container_policy Async::Service::Policy.new(maximum_failures: 5, window: 60)
37
+
38
+ service "web" do
39
+ # Your service configuration.
40
+ end
41
+
42
+ service "worker" do
43
+ # Also uses the same policy.
44
+ end
45
+ ```
46
+
47
+ The policy applies to **all services** in the configuration file.
48
+
49
+ ### Choosing a Threshold
50
+
51
+ Consider your service characteristics:
52
+
53
+ **Strict (catch problems immediately):**
54
+ ``` ruby
55
+ container_policy Async::Service::Policy.new(maximum_failures: 1, window: 5)
56
+ ```
57
+
58
+ **Balanced (tolerate transient issues):**
59
+ ``` ruby
60
+ container_policy Async::Service::Policy.new(maximum_failures: 5, window: 60)
61
+ ```
62
+
63
+ **Lenient (allow many retries):**
64
+ ``` ruby
65
+ container_policy Async::Service::Policy.new(maximum_failures: 20, window: 60)
66
+ ```
67
+
68
+ Factors to consider:
69
+ - **Traffic volume**: High-traffic services may have more absolute failures.
70
+ - **Error types**: Some errors are transient (network timeouts, rate limits).
71
+ - **Dependencies**: Upstream services may need time to recover.
72
+ - **Deployment environment**: Kubernetes/systemd handle restarts, local dev doesn't.
73
+
74
+ ## Per-Container Policy Instances
75
+
76
+ The `container_policy` method accepts a block that's evaluated **each time a container is created**:
77
+
78
+ ``` ruby
79
+ # config/service.rb
80
+ container_policy do
81
+ # This block is called for EACH container created
82
+ # Each container gets its own policy instance with fresh state
83
+ Async::Service::Policy.new(maximum_failures: 5, window: 60)
84
+ end
85
+ ```
86
+
87
+ If your policy is tracking per-container state, this will ensure each container has new policy with clean state.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "loader"
7
7
  require_relative "generic_service"
@@ -52,11 +52,15 @@ module Async
52
52
  end
53
53
 
54
54
  # Initialize an empty configuration.
55
- def initialize(environments = [])
55
+ # @parameter environments [Array] Environment instances.
56
+ # @parameter container_policy [Proc] Optional proc that returns a policy for container lifecycle management.
57
+ def initialize(environments = [], container_policy: nil)
56
58
  @environments = environments
59
+ @container_policy = container_policy
57
60
  end
58
61
 
59
62
  attr :environments
63
+ attr_accessor :container_policy
60
64
 
61
65
  # Check if the configuration is empty.
62
66
  # @returns [Boolean] True if no environments are configured.
@@ -84,11 +88,22 @@ module Async
84
88
 
85
89
  # Create a controller for the configured services.
86
90
  #
91
+ # @parameter container_policy [Proc] A proc that returns the policy to use for managing child lifecycle events.
92
+ # @parameter options [Hash] Additional options passed to the controller.
87
93
  # @returns [Controller] A controller that can be used to start/stop services.
88
- def controller(**options)
89
- Controller.new(self.services(**options).to_a)
94
+ def make_controller(container_policy: @container_policy, implementing: nil, **options)
95
+ controller = Controller.new(self.services(implementing: implementing).to_a, **options)
96
+
97
+ if container_policy
98
+ controller.define_singleton_method(:make_policy, &container_policy)
99
+ end
100
+
101
+ return controller
90
102
  end
91
103
 
104
+ # Alias for backwards compatibility.
105
+ alias controller make_controller
106
+
92
107
  # Add the environment to the configuration.
93
108
  def add(environment)
94
109
  @environments << environment
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require "async/container/controller"
7
+ require_relative "policy"
7
8
 
8
9
  module Async
9
10
  module Service
@@ -13,32 +14,11 @@ module Async
13
14
  # within containers. It extends Async::Container::Controller to provide
14
15
  # service-specific functionality.
15
16
  class Controller < Async::Container::Controller
16
- # Warm up the Ruby process by preloading gems and running GC.
17
- def self.warmup
18
- begin
19
- require "bundler"
20
- Bundler.require(:preload)
21
- rescue Bundler::GemfileNotFound, LoadError
22
- # Ignore.
23
- end
24
-
25
- if Process.respond_to?(:warmup)
26
- Process.warmup
27
- elsif GC.respond_to?(:compact)
28
- 3.times{GC.start}
29
- GC.compact
30
- end
31
- end
32
-
33
17
  # Run a configuration of services.
34
18
  # @parameter configuration [Configuration] The service configuration to run.
35
19
  # @parameter options [Hash] Additional options for the controller.
36
20
  def self.run(configuration, **options)
37
- controller = Async::Service::Controller.new(configuration.services.to_a, **options)
38
-
39
- self.warmup
40
-
41
- controller.run
21
+ configuration.make_controller(**options).run
42
22
  end
43
23
 
44
24
  # Create a controller for the given services.
@@ -58,10 +38,34 @@ module Async
58
38
  @services = services
59
39
  end
60
40
 
41
+ # Warm up the Ruby process by preloading gems, running GC, and compacting memory.
42
+ # This reduces startup latency and improves copy-on-write efficiency.
43
+ def warmup
44
+ begin
45
+ require "bundler"
46
+ Bundler.require(:preload)
47
+ rescue Bundler::GemfileNotFound, LoadError
48
+ # Ignore.
49
+ end
50
+
51
+ if ::Process.respond_to?(:warmup)
52
+ ::Process.warmup
53
+ elsif ::GC.respond_to?(:compact)
54
+ 3.times{::GC.start}
55
+ ::GC.compact
56
+ end
57
+ end
58
+
61
59
  # All the services associated with this controller.
62
60
  # @attribute [Array(Async::Service::Generic)]
63
61
  attr :services
64
62
 
63
+ # Create a policy for managing child lifecycle events.
64
+ # @returns [Policy] The service-level policy with failure rate monitoring.
65
+ def make_policy
66
+ Policy::DEFAULT
67
+ end
68
+
65
69
  # Start all named services.
66
70
  def start
67
71
  @services.each do |service|
@@ -69,6 +73,8 @@ module Async
69
73
  end
70
74
 
71
75
  super
76
+
77
+ self.warmup
72
78
  end
73
79
 
74
80
  # Setup all services into the given container.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025-2026, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module Service
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  # Compatibility shim for Async::Service::GenericService
7
7
  # Use Async::Service::Generic instead
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  # Compatibility shim for Async::Service::HealthChecker
7
7
  # Use Async::Service::Managed::HealthChecker instead
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "environment"
7
7
 
@@ -58,6 +58,18 @@ module Async
58
58
 
59
59
  @configuration.add(self.environment(**options, &block))
60
60
  end
61
+
62
+ # Set the container policy for all services in this configuration.
63
+ # Can be called with either an argument or a block.
64
+ # @parameter value [Async::Container::Policy] The policy to use for managing child lifecycle events.
65
+ # @parameter block [Proc] A block that returns a policy instance.
66
+ def container_policy(value = nil, &block)
67
+ if @configuration.container_policy
68
+ Console.warn(self, "Container policy is already set, overriding previous value!")
69
+ end
70
+
71
+ @configuration.container_policy = block_given? ? block : proc{value}
72
+ end
61
73
  end
62
74
  end
63
75
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  # Compatibility shim for Async::Service::ManagedEnvironment
7
7
  # Use Async::Service::Managed::Environment instead
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  # Compatibility shim for Async::Service::ManagedService
7
7
  # Use Async::Service::Managed::Service instead
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/container/policy"
7
+
8
+ module Async
9
+ module Service
10
+ # A service-level policy that extends the base container policy with failure rate monitoring.
11
+ # This policy will stop the container if the failure rate exceeds a threshold.
12
+ class Policy < Async::Container::Policy
13
+ # Initialize the policy.
14
+ # @parameter maximum_failures [Integer] The maximum number of failures allowed within the window.
15
+ # @parameter window [Integer] The time window in seconds for counting failures.
16
+ def initialize(maximum_failures: 6, window: 60)
17
+ @maximum_failures = maximum_failures
18
+ @window = window
19
+
20
+ @failure_rate_threshold = maximum_failures.to_f / window
21
+ end
22
+
23
+ # The maximum number of failures allowed within the window.
24
+ # @attribute [Integer]
25
+ attr :maximum_failures
26
+
27
+ # The time window in seconds for statistics tracking.
28
+ # @attribute [Integer]
29
+ attr :window
30
+
31
+ # The failure rate threshold in failures per second.
32
+ # @attribute [Float]
33
+ attr :failure_rate_threshold
34
+
35
+ # Create statistics for a container with the configured window.
36
+ # @returns [Async::Container::Statistics] A new statistics instance.
37
+ def make_statistics
38
+ Async::Container::Statistics.new(window: @window)
39
+ end
40
+
41
+ # Called when a child exits. Monitors failure rate and stops the container if threshold is exceeded.
42
+ # @parameter container [Async::Container::Generic] The container.
43
+ # @parameter child [Child] The child process.
44
+ # @parameter status [Process::Status] The exit status.
45
+ # @parameter name [String] The name of the child.
46
+ # @parameter key [Symbol] An optional key for the child.
47
+ # @parameter options [Hash] Additional options for future extensibility.
48
+ def child_exit(container, child, status, name:, key:, **options)
49
+ unless success?(status)
50
+ # Check failure rate after this failure is recorded
51
+ rate = container.statistics.failure_rate.per_second
52
+
53
+ if rate > @failure_rate_threshold
54
+ # Only stop if container is still running (avoid redundant stop calls during shutdown)
55
+ if container.running?
56
+ Console.error(self, "Failure rate exceeded threshold, stopping container!",
57
+ rate: rate,
58
+ threshold: @failure_rate_threshold
59
+ )
60
+ container.stop(true)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # The default service policy instance.
67
+ DEFAULT = self.new.freeze
68
+ end
69
+ end
70
+ end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  # @namespace
7
7
  module Async
8
8
  # @namespace
9
9
  module Service
10
- VERSION = "0.19.1"
10
+ VERSION = "0.20.0"
11
11
  end
12
12
  end
data/lib/async/service.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "service/configuration"
7
7
  require_relative "service/controller"
data/readme.md CHANGED
@@ -19,6 +19,8 @@ Please see the [project documentation](https://socketry.github.io/async-service/
19
19
 
20
20
  - [Getting Started](https://socketry.github.io/async-service/guides/getting-started/index) - This guide explains how to get started with `async-service` to create and run services in Ruby.
21
21
 
22
+ - [Container Policies](https://socketry.github.io/async-service/guides/policies/index) - This guide explains how to configure container policies for your services and understand the default failure handling behavior.
23
+
22
24
  - [Service Architecture](https://socketry.github.io/async-service/guides/service-architecture/index) - This guide explains the key architectural components of `async-service` and how they work together to provide a clean separation of concerns.
23
25
 
24
26
  - [Best Practices](https://socketry.github.io/async-service/guides/best-practices/index) - This guide outlines recommended patterns and practices for building robust, maintainable services with `async-service`.
@@ -29,6 +31,13 @@ Please see the [project documentation](https://socketry.github.io/async-service/
29
31
 
30
32
  Please see the [project releases](https://socketry.github.io/async-service/releases/index) for all releases.
31
33
 
34
+ ### v0.20.0
35
+
36
+ - Introduce `Async::Service::Policy` for monitoring service health and implementing failure handling strategies. Default threshold: 6 failures in 60 seconds (0.1 failures/second).
37
+ - Add `container_policy` configuration method for specifying custom policies in service configuration files.
38
+ - Add `Configuration#make_controller` (aliased as `controller`) for creating controllers with policy injection.
39
+ - Add `Service::Controller#make_policy` which returns `Service::Policy::DEFAULT` by default.
40
+
32
41
  ### v0.19.0
33
42
 
34
43
  - Renamed `Async::Service::GenericService` -\> `Async::Service::Generic`, added compatibility alias for `GenericService`.
@@ -74,13 +83,6 @@ Please see the [project releases](https://socketry.github.io/async-service/relea
74
83
 
75
84
  - Introduce `ContainerEnvironment` and `ContainerService` for implementing best-practice services.
76
85
 
77
- ### v0.13.0
78
-
79
- - Fix null services handling.
80
- - Modernize code and improve documentation.
81
- - Make service name optional and improve code comments.
82
- - Add `respond_to_missing?` for completeness.
83
-
84
86
  ## Contributing
85
87
 
86
88
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Releases
2
2
 
3
+ ## v0.20.0
4
+
5
+ - Introduce `Async::Service::Policy` for monitoring service health and implementing failure handling strategies. Default threshold: 6 failures in 60 seconds (0.1 failures/second).
6
+ - Add `container_policy` configuration method for specifying custom policies in service configuration files.
7
+ - Add `Configuration#make_controller` (aliased as `controller`) for creating controllers with policy injection.
8
+ - Add `Service::Controller#make_policy` which returns `Service::Policy::DEFAULT` by default.
9
+
3
10
  ## v0.19.0
4
11
 
5
12
  - Renamed `Async::Service::GenericService` -\> `Async::Service::Generic`, added compatibility alias for `GenericService`.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-service
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.1
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.29'
61
+ version: '0.33'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.29'
68
+ version: '0.33'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: string-format
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -90,6 +90,7 @@ files:
90
90
  - context/deployment.md
91
91
  - context/getting-started.md
92
92
  - context/index.yaml
93
+ - context/policies.md
93
94
  - context/service-architecture.md
94
95
  - lib/async/service.rb
95
96
  - lib/async/service/configuration.rb
@@ -105,6 +106,7 @@ files:
105
106
  - lib/async/service/managed/service.rb
106
107
  - lib/async/service/managed_environment.rb
107
108
  - lib/async/service/managed_service.rb
109
+ - lib/async/service/policy.rb
108
110
  - lib/async/service/version.rb
109
111
  - license.md
110
112
  - readme.md
metadata.gz.sig CHANGED
Binary file