async-service 0.15.1 → 0.17.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.
@@ -3,63 +3,13 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
+ # Compatibility shim for Async::Service::Generic
7
+ # Use Async::Service::GenericService instead
8
+ require_relative "generic_service"
9
+
6
10
  module Async
7
11
  module Service
8
- # Captures the stateful behaviour of a specific service.
9
- # Specifies the interfaces required by derived classes.
10
- #
11
- # Designed to be invoked within an {Async::Controller::Container}.
12
- class Generic
13
- # Convert the given environment into a service if possible.
14
- # @parameter environment [Environment] The environment to use to construct the service.
15
- # @returns [Generic | Nil] The constructed service if the environment specifies a service class.
16
- def self.wrap(environment)
17
- evaluator = environment.evaluator
18
-
19
- if evaluator.key?(:service_class)
20
- if service_class = evaluator.service_class
21
- return service_class.new(environment, evaluator)
22
- end
23
- end
24
- end
25
-
26
- # Initialize the service from the given environment.
27
- # @parameter environment [Environment]
28
- def initialize(environment, evaluator = environment.evaluator)
29
- @environment = environment
30
- @evaluator = evaluator
31
- end
32
-
33
- # @attribute [Environment] The environment which is used to configure the service.
34
- attr :environment
35
-
36
- # Convert the service evaluator to a hash.
37
- # @returns [Hash] A hash representation of the evaluator.
38
- def to_h
39
- @evaluator.to_h
40
- end
41
-
42
- # The name of the service - used for informational purposes like logging.
43
- # e.g. `myapp.com`.
44
- def name
45
- @evaluator.name
46
- end
47
-
48
- # Start the service. Called before the container setup.
49
- def start
50
- Console.debug(self){"Starting service #{self.name}..."}
51
- end
52
-
53
- # Setup the service into the specified container.
54
- # @parameter container [Async::Container::Generic]
55
- def setup(container)
56
- Console.debug(self){"Setting up service #{self.name}..."}
57
- end
58
-
59
- # Stop the service. Called after the container is stopped.
60
- def stop(graceful = true)
61
- Console.debug(self){"Stopping service #{self.name}..."}
62
- end
63
- end
12
+ # @deprecated Use {GenericService} instead.
13
+ Generic = GenericService
64
14
  end
65
15
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
+
6
+ module Async
7
+ module Service
8
+ # Captures the stateful behaviour of a specific service.
9
+ # Specifies the interfaces required by derived classes.
10
+ #
11
+ # Designed to be invoked within an {Async::Controller::Container}.
12
+ class GenericService
13
+ # Convert the given environment into a service if possible.
14
+ # @parameter environment [Environment] The environment to use to construct the service.
15
+ # @returns [GenericService | Nil] The constructed service if the environment specifies a service class.
16
+ def self.wrap(environment)
17
+ evaluator = environment.evaluator
18
+
19
+ if evaluator.key?(:service_class)
20
+ if service_class = evaluator.service_class
21
+ return service_class.new(environment, evaluator)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Initialize the service from the given environment.
27
+ # @parameter environment [Environment]
28
+ def initialize(environment, evaluator = environment.evaluator)
29
+ @environment = environment
30
+ @evaluator = evaluator
31
+ end
32
+
33
+ # @attribute [Environment] The environment which is used to configure the service.
34
+ attr :environment
35
+
36
+ # Convert the service evaluator to a hash.
37
+ # @returns [Hash] A hash representation of the evaluator.
38
+ def to_h
39
+ @evaluator.to_h
40
+ end
41
+
42
+ # The name of the service - used for informational purposes like logging.
43
+ # e.g. `myapp.com`.
44
+ def name
45
+ @evaluator.name
46
+ end
47
+
48
+ # Start the service. Called before the container setup.
49
+ def start
50
+ Console.debug(self){"Starting service #{self.name}..."}
51
+ end
52
+
53
+ # Setup the service into the specified container.
54
+ # @parameter container [Async::Container::Generic]
55
+ def setup(container)
56
+ Console.debug(self){"Setting up service #{self.name}..."}
57
+ end
58
+
59
+ # Stop the service. Called after the container is stopped.
60
+ def stop(graceful = true)
61
+ Console.debug(self){"Stopping service #{self.name}..."}
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Async
7
+ module Service
8
+ # A health checker for managed services.
9
+ module HealthChecker
10
+ # Start the health checker.
11
+ #
12
+ # If a timeout is specified, a transient child task will be scheduled, which will yield the instance if a block is given, then mark the instance as ready, and finally sleep for half the health check duration (so that we guarantee that the health check runs in time).
13
+ #
14
+ # If a timeout is not specified, the health checker will yield the instance immediately and then mark the instance as ready.
15
+ #
16
+ # @parameter instance [Object] The service instance to check.
17
+ # @parameter timeout [Numeric] The timeout duration for the health check.
18
+ # @parameter parent [Async::Task] The parent task to run the health checker in.
19
+ # @yields {|instance| ...} If a block is given, it will be called with the service instance at least once.
20
+ def health_checker(instance, timeout = @evaluator.health_check_timeout, parent: Async::Task.current, &block)
21
+ if timeout
22
+ parent.async(transient: true) do
23
+ while true
24
+ if block_given?
25
+ yield(instance)
26
+ end
27
+
28
+ # We deliberately create a fiber here, to confirm that fiber creation is working.
29
+ # If something has gone wrong with fiber allocation, we will crash here, and that's okay.
30
+ Fiber.new do
31
+ instance.ready!
32
+ end.resume
33
+
34
+ sleep(timeout / 2)
35
+ end
36
+ end
37
+ else
38
+ if block_given?
39
+ yield(instance)
40
+ end
41
+
42
+ instance.ready!
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
+
6
+ module Async
7
+ module Service
8
+ # Default configuration for managed services.
9
+ #
10
+ # This is provided not because it is required, but to offer a sensible default for production services, and to expose a consistent interface for service configuration.
11
+ module ManagedEnvironment
12
+ # Number of instances to start. By default, when `nil`, uses `Etc.nprocessors`.
13
+ #
14
+ # @returns [Integer | nil] The number of instances to start, or `nil` to use the default.
15
+ def count
16
+ nil
17
+ end
18
+
19
+ # The timeout duration for the startup. Set to `nil` to disable the startup timeout.
20
+ #
21
+ # @returns [Numeric | nil] The startup timeout in seconds.
22
+ def startup_timeout
23
+ nil
24
+ end
25
+
26
+ # The timeout duration for the health check. Set to `nil` to disable the health check.
27
+ #
28
+ # @returns [Numeric | nil] The health check timeout in seconds.
29
+ def health_check_timeout
30
+ 30
31
+ end
32
+
33
+ # Options to use when creating the container, including `restart`, `count`, and `health_check_timeout`.
34
+ #
35
+ # @returns [Hash] The options for the container.
36
+ def container_options
37
+ {
38
+ restart: true,
39
+ count: self.count,
40
+ startup_timeout: self.startup_timeout,
41
+ health_check_timeout: self.health_check_timeout,
42
+ }.compact
43
+ end
44
+
45
+ # Any scripts to preload before starting the service.
46
+ #
47
+ # @returns [Array(String)] The list of scripts to preload.
48
+ def preload
49
+ []
50
+ end
51
+
52
+ # General tags for metrics, traces and logging.
53
+ #
54
+ # @returns [Array(String)] The tags for the service.
55
+ def tags
56
+ []
57
+ end
58
+
59
+ # Prepare the instance for running the service.
60
+ #
61
+ # This is called before {Async::Service::ManagedService#run}.
62
+ #
63
+ # @parameter instance [Object] The container instance.
64
+ def prepare!(instance)
65
+ # No preparation required by default.
66
+ end
67
+ end
68
+ end
69
+ end
70
+
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
+
6
+ require_relative "generic_service"
7
+ require_relative "health_checker"
8
+
9
+ module Async
10
+ module Service
11
+ # A managed service with built-in health checking, restart policies, and process title formatting.
12
+ #
13
+ # This is the recommended base class for most services that need robust lifecycle management.
14
+ class ManagedService < GenericService
15
+ include HealthChecker
16
+
17
+ private def format_title(evaluator, server)
18
+ "#{evaluator.name} #{server.to_s}"
19
+ end
20
+
21
+ # Run the service logic.
22
+ #
23
+ # Override this method to implement your service. Return an object that represents the running service (e.g., a server, task, or worker pool) for health checking.
24
+ #
25
+ # @parameter instance [Object] The container instance.
26
+ # @parameter evaluator [Environment::Evaluator] The environment evaluator.
27
+ # @returns [Object] The service object (server, task, etc.)
28
+ def run(instance, evaluator)
29
+ Async do
30
+ sleep
31
+ end
32
+ end
33
+
34
+ # Preload any resources specified by the environment.
35
+ def preload!
36
+ if scripts = @evaluator.preload
37
+ root = @evaluator.root
38
+ scripts = Array(scripts)
39
+
40
+ scripts.each do |path|
41
+ Console.info(self){"Preloading #{path}..."}
42
+ full_path = File.expand_path(path, root)
43
+ require(full_path)
44
+ end
45
+ end
46
+ rescue StandardError, LoadError => error
47
+ Console.warn(self, "Service preload failed!", error)
48
+ end
49
+
50
+ # Start the service, including preloading resources.
51
+ def start
52
+ preload!
53
+
54
+ super
55
+ end
56
+
57
+ # Called after the service has been prepared but before it starts running.
58
+ #
59
+ # Override this method to emit metrics, logs, or perform other actions when the service preparation is complete.
60
+ #
61
+ # @parameter instance [Async::Container::Instance] The container instance.
62
+ # @parameter start_time [Async::Clock] The monotonic start time from {Async::Clock.start}.
63
+ def emit_prepared(instance, start_time)
64
+ # Override in subclasses as needed.
65
+ end
66
+
67
+ # Called after the service has started running.
68
+ #
69
+ # Override this method to emit metrics, logs, or perform other actions when the service begins running.
70
+ #
71
+ # @parameter instance [Async::Container::Instance] The container instance.
72
+ # @parameter start_time [Async::Clock] The monotonic start time from {Async::Clock.start}.
73
+ def emit_running(instance, start_time)
74
+ # Override in subclasses as needed.
75
+ end
76
+
77
+ # Set up the container with health checking and process title formatting.
78
+ # @parameter container [Async::Container] The container to configure.
79
+ def setup(container)
80
+ super
81
+
82
+ container_options = @evaluator.container_options
83
+ health_check_timeout = container_options[:health_check_timeout]
84
+
85
+ container.run(**container_options) do |instance|
86
+ start_time = Async::Clock.start
87
+
88
+ Async do
89
+ evaluator = self.environment.evaluator
90
+
91
+ instance.status!("Preparing...")
92
+ evaluator.prepare!(instance)
93
+ emit_prepared(instance, start_time)
94
+
95
+ instance.status!("Running...")
96
+ server = run(instance, evaluator)
97
+ emit_running(instance, start_time)
98
+
99
+ health_checker(instance) do
100
+ instance.name = format_title(evaluator, server)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Service
8
- VERSION = "0.15.1"
8
+ VERSION = "0.17.0"
9
9
  end
10
10
  end
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2024-2025, by Samuel Williams.
3
+ Copyright, 2024-2026, by Samuel Williams.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -23,10 +23,23 @@ Please see the [project documentation](https://socketry.github.io/async-service/
23
23
 
24
24
  - [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`.
25
25
 
26
+ - [Deployment](https://socketry.github.io/async-service/guides/deployment/index) - This guide explains how to deploy `async-service` applications using systemd and Kubernetes. We'll use a simple example service to demonstrate deployment configurations.
27
+
26
28
  ## Releases
27
29
 
28
30
  Please see the [project releases](https://socketry.github.io/async-service/releases/index) for all releases.
29
31
 
32
+ ### v0.17.0
33
+
34
+ - `ManagedService` now sends `status!` messages during startup to prevent premature health check timeouts for slow-starting services.
35
+ - Support for `startup_timeout` option via `container_options` to detect processes that hang during startup and never become ready.
36
+
37
+ ### v0.16.0
38
+
39
+ - Renamed `Async::Service::Generic` -\> `Async::Service::GenericService`, added compatibilty alias.
40
+ - Renamed `Async::Service::Managed::Service` -\> `Async::Service::ManagedService`.
41
+ - Renamed `Async::Service::Managed::Environment` -\> `Async::Service::ManagedEnvironment`.
42
+
30
43
  ### v0.15.1
31
44
 
32
45
  - `Managed::Service` should run within `Async do ... end`.
@@ -64,14 +77,6 @@ Please see the [project releases](https://socketry.github.io/async-service/relea
64
77
  - Add `Environment::Evaluator#as_json` for JSON serialization support.
65
78
  - Allow constructing a configuration with existing environments.
66
79
 
67
- ### v0.9.0
68
-
69
- - Allow providing a list of modules to include in environments.
70
-
71
- ### v0.8.0
72
-
73
- - Introduce `Environment#implements?` and related methods for interface checking.
74
-
75
80
  ## Contributing
76
81
 
77
82
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Releases
2
2
 
3
+ ## v0.17.0
4
+
5
+ - `ManagedService` now sends `status!` messages during startup to prevent premature health check timeouts for slow-starting services.
6
+ - Support for `startup_timeout` option via `container_options` to detect processes that hang during startup and never become ready.
7
+
8
+ ## v0.16.0
9
+
10
+ - Renamed `Async::Service::Generic` -\> `Async::Service::GenericService`, added compatibilty alias.
11
+ - Renamed `Async::Service::Managed::Service` -\> `Async::Service::ManagedService`.
12
+ - Renamed `Async::Service::Managed::Environment` -\> `Async::Service::ManagedEnvironment`.
13
+
3
14
  ## v0.15.1
4
15
 
5
16
  - `Managed::Service` should run within `Async do ... end`.
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.15.1
4
+ version: 0.17.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.16'
61
+ version: '0.28'
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.16'
68
+ version: '0.28'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: string-format
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -87,6 +87,7 @@ extra_rdoc_files: []
87
87
  files:
88
88
  - bin/async-service
89
89
  - context/best-practices.md
90
+ - context/deployment.md
90
91
  - context/getting-started.md
91
92
  - context/index.yaml
92
93
  - context/service-architecture.md
@@ -96,11 +97,11 @@ files:
96
97
  - lib/async/service/environment.rb
97
98
  - lib/async/service/formatting.rb
98
99
  - lib/async/service/generic.rb
100
+ - lib/async/service/generic_service.rb
101
+ - lib/async/service/health_checker.rb
99
102
  - lib/async/service/loader.rb
100
- - lib/async/service/managed.rb
101
- - lib/async/service/managed/environment.rb
102
- - lib/async/service/managed/health_checker.rb
103
- - lib/async/service/managed/service.rb
103
+ - lib/async/service/managed_environment.rb
104
+ - lib/async/service/managed_service.rb
104
105
  - lib/async/service/version.rb
105
106
  - license.md
106
107
  - readme.md
@@ -125,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
126
  - !ruby/object:Gem::Version
126
127
  version: '0'
127
128
  requirements: []
128
- rubygems_version: 3.6.9
129
+ rubygems_version: 4.0.3
129
130
  specification_version: 4
130
131
  summary: A service layer for Async.
131
132
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
5
-
6
- module Async
7
- module Service
8
- module Managed
9
- # Default configuration for managed services.
10
- #
11
- # This is provided not because it is required, but to offer a sensible default for production services, and to expose a consistent interface for service configuration.
12
- module Environment
13
- # Number of instances to start. By default, when `nil`, uses `Etc.nprocessors`.
14
- #
15
- # @returns [Integer | nil] The number of instances to start, or `nil` to use the default.
16
- def count
17
- nil
18
- end
19
-
20
- # The timeout duration for the health check. Set to `nil` to disable the health check.
21
- #
22
- # @returns [Numeric | nil] The health check timeout in seconds.
23
- def health_check_timeout
24
- 30
25
- end
26
-
27
- # Options to use when creating the container, including `restart`, `count`, and `health_check_timeout`.
28
- #
29
- # @returns [Hash] The options for the container.
30
- def container_options
31
- {
32
- restart: true,
33
- count: self.count,
34
- health_check_timeout: self.health_check_timeout,
35
- }.compact
36
- end
37
-
38
- # Any scripts to preload before starting the service.
39
- #
40
- # @returns [Array(String)] The list of scripts to preload.
41
- def preload
42
- []
43
- end
44
- end
45
- end
46
- end
47
- end
48
-
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
5
-
6
- module Async
7
- module Service
8
- module Managed
9
- # A health checker for managed services.
10
- module HealthChecker
11
- # Start the health checker.
12
- #
13
- # If a timeout is specified, a transient child task will be scheduled, which will yield the instance if a block is given, then mark the instance as ready, and finally sleep for half the health check duration (so that we guarantee that the health check runs in time).
14
- #
15
- # If a timeout is not specified, the health checker will yield the instance immediately and then mark the instance as ready.
16
- #
17
- # @parameter instance [Object] The service instance to check.
18
- # @parameter timeout [Numeric] The timeout duration for the health check.
19
- # @parameter parent [Async::Task] The parent task to run the health checker in.
20
- # @yields {|instance| ...} If a block is given, it will be called with the service instance at least once.
21
- def health_checker(instance, timeout = @evaluator.health_check_timeout, parent: Async::Task.current, &block)
22
- if timeout
23
- parent.async(transient: true) do
24
- while true
25
- if block_given?
26
- yield(instance)
27
- end
28
-
29
- # We deliberately create a fiber here, to confirm that fiber creation is working.
30
- # If something has gone wrong with fiber allocation, we will crash here, and that's okay.
31
- Fiber.new do
32
- instance.ready!
33
- end.resume
34
-
35
- sleep(timeout / 2)
36
- end
37
- end
38
- else
39
- if block_given?
40
- yield(instance)
41
- end
42
-
43
- instance.ready!
44
- end
45
- end
46
- end
47
- end
48
- end
49
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
5
-
6
- require_relative "../generic"
7
- require_relative "../formatting"
8
- require_relative "health_checker"
9
-
10
- module Async
11
- module Service
12
- module Managed
13
- # A managed service with built-in health checking, restart policies, and process title formatting.
14
- #
15
- # This is the recommended base class for most services that need robust lifecycle management.
16
- class Service < Generic
17
- include Formatting
18
- include HealthChecker
19
-
20
- private def format_title(evaluator, server)
21
- "#{evaluator.name} #{server.to_s}"
22
- end
23
-
24
- # Run the service logic.
25
- #
26
- # Override this method to implement your service. Return an object that represents the running service (e.g., a server, task, or worker pool) for health checking.
27
- #
28
- # @parameter instance [Object] The container instance.
29
- # @parameter evaluator [Environment::Evaluator] The environment evaluator.
30
- # @returns [Object] The service object (server, task, etc.)
31
- def run(instance, evaluator)
32
- Async do
33
- sleep
34
- end
35
- end
36
-
37
- # Preload any resources specified by the environment.
38
- def preload!
39
- if scripts = @evaluator.preload
40
- root = @evaluator.root
41
- scripts = Array(scripts)
42
-
43
- scripts.each do |path|
44
- Console.info(self){"Preloading #{path}..."}
45
- full_path = File.expand_path(path, root)
46
- require(full_path)
47
- end
48
- end
49
- rescue StandardError, LoadError => error
50
- Console.warn(self, "Service preload failed!", error)
51
- end
52
-
53
- # Start the service, including preloading resources.
54
- def start
55
- preload!
56
-
57
- super
58
- end
59
-
60
- # Set up the container with health checking and process title formatting.
61
- # @parameter container [Async::Container] The container to configure.
62
- def setup(container)
63
- super
64
-
65
- container_options = @evaluator.container_options
66
- health_check_timeout = container_options[:health_check_timeout]
67
-
68
- container.run(**container_options) do |instance|
69
- Async do
70
- evaluator = self.environment.evaluator
71
-
72
- server = run(instance, evaluator)
73
-
74
- health_checker(instance) do
75
- instance.name = format_title(evaluator, server)
76
- end
77
- end
78
- end
79
- end
80
- end
81
- end
82
- end
83
- end