async-service 0.13.0 → 0.14.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.
@@ -5,8 +5,17 @@
5
5
 
6
6
  module Async
7
7
  module Service
8
+ # Represents a service configuration with lazy evaluation and module composition.
9
+ #
10
+ # Environments store configuration as methods that can be overridden and composed using Ruby modules. They support lazy evaluation through evaluators.
8
11
  class Environment
12
+ # A builder for constructing environments using a DSL.
9
13
  class Builder < BasicObject
14
+ # Create a new environment with facets and values.
15
+ # @parameter facets [Array(Module)] Modules to include in the environment.
16
+ # @parameter values [Hash] Key-value pairs to define as methods.
17
+ # @parameter block [Proc] A block for additional configuration.
18
+ # @returns [Module] The constructed environment module.
10
19
  def self.for(*facets, **values, &block)
11
20
  top = ::Module.new
12
21
 
@@ -46,10 +55,14 @@ module Async
46
55
  return top
47
56
  end
48
57
 
58
+ # Initialize a new builder.
59
+ # @parameter facet [Module] The module to build into, defaults to a new `Module`.
49
60
  def initialize(facet = ::Module.new)
50
61
  @facet = facet
51
62
  end
52
63
 
64
+ # Include a module or other includable object into the environment.
65
+ # @parameter target [Module] The module to include.
53
66
  def include(target)
54
67
  if target.class == ::Module
55
68
  @facet.include(target)
@@ -60,6 +73,10 @@ module Async
60
73
  end
61
74
  end
62
75
 
76
+ # Define methods dynamically on the environment.
77
+ # @parameter name [Symbol] The method name to define.
78
+ # @parameter argument [Object] The value to return from the method.
79
+ # @parameter block [Proc] A block to use as the method implementation.
63
80
  def method_missing(name, argument = nil, &block)
64
81
  if block
65
82
  @facet.define_method(name, &block)
@@ -68,15 +85,25 @@ module Async
68
85
  end
69
86
  end
70
87
 
88
+ # Always respond to missing methods for dynamic method definition.
89
+ # @parameter name [Symbol] The method name.
90
+ # @parameter include_private [Boolean] Whether to include private methods.
91
+ # @returns [Boolean] Always true to enable dynamic method definition.
71
92
  def respond_to_missing?(name, include_private = false)
72
93
  true
73
94
  end
74
95
  end
75
96
 
97
+ # Build a new environment using the builder DSL.
98
+ # @parameter arguments [Array] Arguments passed to Builder.for
99
+ # @returns [Environment] A new environment instance.
76
100
  def self.build(...)
77
101
  Environment.new(Builder.for(...))
78
102
  end
79
103
 
104
+ # Initialize a new environment.
105
+ # @parameter facet [Module] The facet module containing the configuration methods.
106
+ # @parameter parent [Environment | Nil] The parent environment for inheritance.
80
107
  def initialize(facet = ::Module.new, parent = nil)
81
108
  unless facet.class == ::Module
82
109
  raise ArgumentError, "Facet must be a module!"
@@ -92,21 +119,32 @@ module Async
92
119
  # @attribute [Environment | Nil] The parent environment, if any.
93
120
  attr :parent
94
121
 
122
+ # Include this environment's facet into a target module.
123
+ # @parameter target [Module] The target module to include into.
95
124
  def included(target)
96
125
  @parent&.included(target)
97
126
  target.include(@facet)
98
127
  end
99
128
 
129
+ # Create a new environment with additional configuration.
130
+ # @parameter arguments [Array] Arguments passed to Environment.build.
131
+ # @returns [Environment] A new environment with this as parent.
100
132
  def with(...)
101
133
  return self.class.new(Builder.for(...), self)
102
134
  end
103
135
 
136
+ # Check if this environment implements a given interface.
137
+ # @parameter interface [Module] The interface to check.
138
+ # @returns [Boolean] True if this environment implements the interface.
104
139
  def implements?(interface)
105
140
  @facet <= interface
106
141
  end
107
142
 
108
143
  # An evaluator is lazy read-only view of an environment. It memoizes all method calls.
109
144
  class Evaluator
145
+ # Create an evaluator wrapper for an environment.
146
+ # @parameter environment [Environment] The environment to wrap.
147
+ # @returns [Evaluator] A new evaluator instance.
110
148
  def self.wrap(environment)
111
149
  evaluator = ::Class.new(self)
112
150
 
@@ -137,14 +175,19 @@ module Async
137
175
  return evaluator.new
138
176
  end
139
177
 
178
+ # Initialize a new evaluator.
140
179
  def initialize
141
180
  @cache = {}
142
181
  end
143
182
 
183
+ # Inspect representation of the evaluator.
184
+ # @returns [String] A string representation of the evaluator with its keys.
144
185
  def inspect
145
186
  "#<#{Evaluator} #{self.keys}>"
146
187
  end
147
188
 
189
+ # Convert the evaluator to a hash.
190
+ # @returns [Hash] A hash with all evaluated keys and values.
148
191
  def to_h
149
192
  # Ensure all keys are evaluated:
150
193
  self.keys.each do |name|
@@ -154,23 +197,38 @@ module Async
154
197
  return @cache
155
198
  end
156
199
 
200
+ # Convert the evaluator to JSON.
201
+ # @parameter arguments [Array] Arguments passed to to_json.
202
+ # @returns [String] A JSON representation of the evaluator.
157
203
  def to_json(...)
158
204
  self.to_h.to_json(...)
159
205
  end
160
206
 
207
+ # Get value for a given key.
208
+ # @parameter key [Symbol] The key to look up.
209
+ # @returns [Object, nil] The value for the key, or nil if not found.
161
210
  def [](key)
162
- self.__send__(key)
211
+ if self.key?(key)
212
+ self.__send__(key)
213
+ end
163
214
  end
164
215
 
216
+ # Check if a key is available.
217
+ # @parameter key [Symbol] The key to check.
218
+ # @returns [Boolean] True if the key exists.
165
219
  def key?(key)
166
220
  self.keys.include?(key)
167
221
  end
168
222
  end
169
223
 
224
+ # Create an evaluator for this environment.
225
+ # @returns [Evaluator] A lazy evaluator for this environment.
170
226
  def evaluator
171
227
  return Evaluator.wrap(self)
172
228
  end
173
229
 
230
+ # Convert the environment to a hash.
231
+ # @returns [Hash] A hash representation of the environment.
174
232
  def to_h
175
233
  evaluator.to_h
176
234
  end
@@ -0,0 +1,84 @@
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
+ # Formatting utilities for service titles.
9
+ #
10
+ # Services need meaningful process/thread names for monitoring and debugging. This module provides consistent formatting for common service metrics like connection counts, request ratios, and load values in process titles.
11
+ #
12
+ # It is expected you will include these into your service class and use them to update the `instance.name` in the health check.
13
+ module Formatting
14
+ UNITS = [nil, "K", "M", "B", "T", "P", "E", "Z", "Y"]
15
+
16
+ # Format a count into a human-readable string.
17
+ # @parameter value [Numeric] The count to format.
18
+ # @parameter units [Array] The units to use for formatting (default: UNITS).
19
+ # @returns [String] A formatted string representing the count.
20
+ def format_count(value, units = UNITS)
21
+ value = value.to_f
22
+ index = 0
23
+ limit = units.size - 1
24
+
25
+ # Handle negative numbers by working with absolute value:
26
+ negative = value < 0
27
+ value = value.abs
28
+
29
+ while value >= 1000 and index < limit
30
+ value = value / 1000
31
+ index += 1
32
+ end
33
+
34
+ result = String.new
35
+ result << "-" if negative
36
+ result << value.round(2).to_s
37
+ result << units[index].to_s if units[index]
38
+
39
+ return result
40
+ end
41
+
42
+ module_function :format_count
43
+
44
+ # Format a ratio as "current/total" with human-readable counts.
45
+ # @parameter current [Numeric] The current value.
46
+ # @parameter total [Numeric] The total value.
47
+ # @returns [String] A formatted ratio string.
48
+ def format_ratio(current, total)
49
+ "#{format_count(current)}/#{format_count(total)}"
50
+ end
51
+
52
+ module_function :format_ratio
53
+
54
+ # Format a load value as a decimal with specified precision.
55
+ # @parameter load [Numeric] The load value (typically 0.0 to 1.0+).
56
+ # @returns [String] A formatted load string.
57
+ def format_load(load)
58
+ load.round(2).to_s
59
+ end
60
+
61
+ module_function :format_load
62
+
63
+ # Format multiple statistics into a compact string.
64
+ # @parameter stats [Hash] Hash of statistic names to values or [current, total] arrays.
65
+ # @returns [String] A formatted statistics string.
66
+ def format_statistics(**pairs)
67
+ pairs.map do |key, value|
68
+ case value
69
+ when Array
70
+ if value.length == 2
71
+ "#{key.to_s.upcase}=#{format_ratio(value[0], value[1])}"
72
+ else
73
+ "#{key.to_s.upcase}=#{value.join('/')}"
74
+ end
75
+ else
76
+ "#{key.to_s.upcase}=#{format_count(value)}"
77
+ end
78
+ end.join(" ")
79
+ end
80
+
81
+ module_function :format_statistics
82
+ end
83
+ end
84
+ end
@@ -11,7 +11,7 @@ module Async
11
11
  # Designed to be invoked within an {Async::Controller::Container}.
12
12
  class Generic
13
13
  # Convert the given environment into a service if possible.
14
- # @parameter environment [Build::Environment] The environment to use to construct the service.
14
+ # @parameter environment [Environment] The environment to use to construct the service.
15
15
  # @returns [Generic | Nil] The constructed service if the environment specifies a service class.
16
16
  def self.wrap(environment)
17
17
  evaluator = environment.evaluator
@@ -24,12 +24,17 @@ module Async
24
24
  end
25
25
 
26
26
  # Initialize the service from the given environment.
27
- # @parameter environment [Build::Environment]
27
+ # @parameter environment [Environment]
28
28
  def initialize(environment, evaluator = environment.evaluator)
29
29
  @environment = environment
30
30
  @evaluator = evaluator
31
31
  end
32
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.
33
38
  def to_h
34
39
  @evaluator.to_h
35
40
  end
@@ -40,7 +45,7 @@ module Async
40
45
  @evaluator.name
41
46
  end
42
47
 
43
- # Start the service.
48
+ # Start the service. Called before the container setup.
44
49
  def start
45
50
  Console.debug(self) {"Starting service #{self.name}..."}
46
51
  end
@@ -51,10 +56,44 @@ module Async
51
56
  Console.debug(self) {"Setting up service #{self.name}..."}
52
57
  end
53
58
 
54
- # Stop the service.
59
+ # Stop the service. Called after the container is stopped.
55
60
  def stop(graceful = true)
56
61
  Console.debug(self) {"Stopping service #{self.name}..."}
57
62
  end
63
+
64
+ protected
65
+
66
+ # Start the health checker.
67
+ #
68
+ # 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).
69
+ #
70
+ # If a timeout is not specified, the health checker will yield the instance immediately and then mark the instance as ready.
71
+ #
72
+ # @parameter instance [Object] The service instance to check.
73
+ # @parameter timeout [Numeric] The timeout duration for the health check.
74
+ # @parameter parent [Async::Task] The parent task to run the health checker in.
75
+ # @yields {|instance| ...} If a block is given, it will be called with the service instance at least once.
76
+ def health_checker(instance, timeout = @evaluator.health_check_timeout, parent: Async::Task.current, &block)
77
+ if timeout
78
+ parent.async(transient: true) do
79
+ while true
80
+ if block_given?
81
+ yield(instance)
82
+ end
83
+
84
+ instance.ready!
85
+
86
+ sleep(timeout / 2)
87
+ end
88
+ end
89
+ else
90
+ if block_given?
91
+ yield(instance)
92
+ end
93
+
94
+ instance.ready!
95
+ end
96
+ end
58
97
  end
59
98
  end
60
99
  end
@@ -38,6 +38,8 @@ module Async
38
38
  loader.instance_eval(File.read(path), path)
39
39
  end
40
40
 
41
+ # Load a file relative to the loader's root directory.
42
+ # @parameter path [String] The path to the file to load.
41
43
  def load_file(path)
42
44
  Loader.load_file(@configuration, File.expand_path(path, @root))
43
45
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module Service
8
- VERSION = "0.13.0"
8
+ VERSION = "0.14.0"
9
9
  end
10
10
  end
data/lib/async/service.rb CHANGED
@@ -6,3 +6,10 @@
6
6
  require_relative "service/configuration"
7
7
  require_relative "service/controller"
8
8
  require_relative "service/version"
9
+
10
+ # @namespace
11
+ module Async
12
+ # @namespace
13
+ module Service
14
+ end
15
+ end
data/readme.md CHANGED
@@ -1,9 +1,76 @@
1
1
  # Async::Service
2
2
 
3
- Provides a simple service interface for configuring and running services.
3
+ Provides a simple service interface for configuring and running asynchronous services in Ruby.
4
4
 
5
5
  [![Development Status](https://github.com/socketry/async-service/workflows/Test/badge.svg)](https://github.com/socketry/async-service/actions?workflow=Test)
6
6
 
7
+ ## Features
8
+
9
+ - **Service Management**: Define, configure, and run long-running services.
10
+ - **Container Integration**: Built on `async-container` for robust process management.
11
+ - **Multiple Services**: Run and coordinate multiple services together.
12
+ - **Automatic Restart**: Services automatically restart on failure.
13
+ - **Graceful Shutdown**: Clean shutdown handling with proper resource cleanup.
14
+ - **Environment Configuration**: Configure services with environment variables and settings.
15
+
16
+ ## Usage
17
+
18
+ Please see the [project documentation](https://socketry.github.io/async-service/) for more details.
19
+
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
+
22
+ - [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
+
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
+
26
+ ## Releases
27
+
28
+ Please see the [project releases](https://socketry.github.io/async-service/releases/index) for all releases.
29
+
30
+ ### v0.14.0
31
+
32
+ - Introduce `ContainerEnvironment` and `ContainerService` for implementing best-practice services.
33
+
34
+ ### v0.13.0
35
+
36
+ - Fix null services handling.
37
+ - Modernize code and improve documentation.
38
+ - Make service name optional and improve code comments.
39
+ - Add `respond_to_missing?` for completeness.
40
+
41
+ ### v0.12.0
42
+
43
+ - Add convenient `Configuration.build{...}` method for constructing inline configurations.
44
+
45
+ ### v0.11.0
46
+
47
+ - Allow builder with argument for more flexible configuration construction.
48
+
49
+ ### v0.10.0
50
+
51
+ - Add `Environment::Evaluator#as_json` for JSON serialization support.
52
+ - Allow constructing a configuration with existing environments.
53
+
54
+ ### v0.9.0
55
+
56
+ - Allow providing a list of modules to include in environments.
57
+
58
+ ### v0.8.0
59
+
60
+ - Introduce `Environment#implements?` and related methods for interface checking.
61
+
62
+ ### v0.7.0
63
+
64
+ - Allow instance methods that take arguments in environments.
65
+
66
+ ### v0.6.1
67
+
68
+ - Fix requirement that facet must be a module.
69
+
70
+ ### v0.6.0
71
+
72
+ - Unify construction of environments for better consistency.
73
+
7
74
  ## Contributing
8
75
 
9
76
  We welcome contributions to this project.
data/releases.md ADDED
@@ -0,0 +1,85 @@
1
+ # Releases
2
+
3
+ ## v0.14.0
4
+
5
+ - Introduce `ContainerEnvironment` and `ContainerService` for implementing best-practice services.
6
+
7
+ ## v0.13.0
8
+
9
+ - Fix null services handling.
10
+ - Modernize code and improve documentation.
11
+ - Make service name optional and improve code comments.
12
+ - Add `respond_to_missing?` for completeness.
13
+
14
+ ## v0.12.0
15
+
16
+ - Add convenient `Configuration.build{...}` method for constructing inline configurations.
17
+
18
+ ## v0.11.0
19
+
20
+ - Allow builder with argument for more flexible configuration construction.
21
+
22
+ ## v0.10.0
23
+
24
+ - Add `Environment::Evaluator#as_json` for JSON serialization support.
25
+ - Allow constructing a configuration with existing environments.
26
+
27
+ ## v0.9.0
28
+
29
+ - Allow providing a list of modules to include in environments.
30
+
31
+ ## v0.8.0
32
+
33
+ - Introduce `Environment#implements?` and related methods for interface checking.
34
+
35
+ ## v0.7.0
36
+
37
+ - Allow instance methods that take arguments in environments.
38
+
39
+ ## v0.6.1
40
+
41
+ - Fix requirement that facet must be a module.
42
+
43
+ ## v0.6.0
44
+
45
+ - Unify construction of environments for better consistency.
46
+
47
+ ## v0.5.1
48
+
49
+ - Relax dependency on async-container for better compatibility.
50
+
51
+ ## v0.5.0
52
+
53
+ - Add support for passing through options to controllers.
54
+
55
+ ## v0.4.0
56
+
57
+ - Reuse evaluator for service instances for better performance.
58
+ - Expose `Configuration.load` and `Controller.start` for better composition.
59
+ - Add simple service example.
60
+
61
+ ## v0.3.1
62
+
63
+ - Fix usage of `raise` in `BasicObject` context.
64
+
65
+ ## v0.3.0
66
+
67
+ - Use modules for environments instead of basic objects.
68
+ - Allow non-modules to be included in environments.
69
+
70
+ ## v0.2.1
71
+
72
+ - Add missing call to `super` in service implementations.
73
+
74
+ ## v0.2.0
75
+
76
+ - Add support for loading other configuration files.
77
+ - Minor bug fixes and improvements.
78
+
79
+ ## v0.1.0
80
+
81
+ - Initial release with core service framework.
82
+ - Environment abstraction for service configuration.
83
+ - Improved evaluator implementation with comprehensive tests.
84
+ - Controller for handling service execution.
85
+ - Support for explicit `service_class` configuration.
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.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -36,7 +36,7 @@ cert_chain:
36
36
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
37
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
38
  -----END CERTIFICATE-----
39
- date: 2025-03-08 00:00:00.000000000 Z
39
+ date: 1980-01-02 00:00:00.000000000 Z
40
40
  dependencies:
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: async
@@ -71,16 +71,25 @@ executables:
71
71
  extensions: []
72
72
  extra_rdoc_files: []
73
73
  files:
74
+ - agent.md
74
75
  - bin/async-service
76
+ - context/best-practices.md
77
+ - context/getting-started.md
78
+ - context/index.yaml
79
+ - context/service-architecture.md
75
80
  - lib/async/service.rb
76
81
  - lib/async/service/configuration.rb
82
+ - lib/async/service/container_environment.rb
83
+ - lib/async/service/container_service.rb
77
84
  - lib/async/service/controller.rb
78
85
  - lib/async/service/environment.rb
86
+ - lib/async/service/formatting.rb
79
87
  - lib/async/service/generic.rb
80
88
  - lib/async/service/loader.rb
81
89
  - lib/async/service/version.rb
82
90
  - license.md
83
91
  - readme.md
92
+ - releases.md
84
93
  homepage: https://github.com/socketry/async-service
85
94
  licenses:
86
95
  - MIT
@@ -94,14 +103,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
103
  requirements:
95
104
  - - ">="
96
105
  - !ruby/object:Gem::Version
97
- version: '3.1'
106
+ version: '3.2'
98
107
  required_rubygems_version: !ruby/object:Gem::Requirement
99
108
  requirements:
100
109
  - - ">="
101
110
  - !ruby/object:Gem::Version
102
111
  version: '0'
103
112
  requirements: []
104
- rubygems_version: 3.6.2
113
+ rubygems_version: 3.6.9
105
114
  specification_version: 4
106
115
  summary: A service layer for Async.
107
116
  test_files: []
metadata.gz.sig CHANGED
Binary file