macaw_framework 1.4.1 → 1.5.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: e463859dd8c04003288cb088a93a4bd664e3c321eb4bd49144e945e5e1582fdd
4
- data.tar.gz: fdb469e8f36de341cfc2b63ea6e9c684f8e8d3c7209521269899be340d3cad29
3
+ metadata.gz: 803f40a0a12304b555d3f998e323b4e475f10194518f566dc98e24c6b3984461
4
+ data.tar.gz: c0e90bf375a372105324e4444419ce05b6362e056bc2d2894820653cf9d7ef0a
5
5
  SHA512:
6
- metadata.gz: ef1722de3015b0b53ba18a969053f8593fa42e0f1b7f16471d474e97833f2341c1070d151e0d213ebcc6174a5bffda96424dccf5923279b1e4c896407c255a47
7
- data.tar.gz: b336869a06aff82b34147f0d76f0c51f5d526c8f8b5da0a7b77c3488f56938b719318e2e538e4bc37eea9b2d936332aa6ec68e2236d596a072cc885e7e2a443f
6
+ metadata.gz: 0d43d8371216644879492cf22e3269aa743b4d7f48638e2e06d6fa72c5d2ee0975611111306742457c57b0d443937b6983ee95c00a8400c897aa6c9ff8a083b9
7
+ data.tar.gz: 7f453d9a093520e088dce07c93c9a818869511dc8b63a9737de6d958edd9ba88281d7f2104a46f0c39db945041de06e2d8adf016eb860d87df2fa406f38292f7
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.0
2
+ TargetRubyVersion: 3.2
3
3
  SuggestExtensions: false
4
4
  NewCops: disable
5
5
 
data/CHANGELOG.md CHANGED
@@ -160,3 +160,51 @@
160
160
 
161
161
  ## [1.4.1] - 2026-02-01
162
162
  - Fixing issue when re-spawning new threads
163
+
164
+ ## [1.4.2] - 2026-02-24
165
+ - Removing unused Rate Limiting Middleware
166
+
167
+ ## [1.4.3] - 2026-03-04
168
+ - Fix critical bug in `sanitize_parameter_value` where the first `gsub` result was discarded, leaving special characters unsanitized
169
+ - Fix header parsing regex to correctly accept real-world headers (Authorization, Cookie, User-Agent, Host with port, etc.)
170
+ - Fix deadlock in `maintain_worker_pool` caused by non-reentrant mutex re-acquisition when respawning dead workers
171
+ - Add missing `require 'digest'` in `LogDataFilter`, preventing `NameError` when sensitive fields are configured
172
+ - Fix thread-safety in `Cache#write` and `Cache#read`: both now always synchronize on the internal mutex
173
+ - Fix thread-safety of `@session` hash: `declare_client_session` now synchronizes on a dedicated mutex
174
+ - Fix `**kwargs` being silently dropped in `LoggingAspect` and `PrometheusAspect` when forwarding to `super`
175
+ - Fix `CacheAspect` holding mutex during entire endpoint execution on cache miss; endpoint now runs outside the lock
176
+ - Remove blocking `sleep(2)` from `MemoryInvalidationMiddleware` constructor; eviction thread no longer halts server startup
177
+ - Remove `sleep(1)` per cron job from `CronRunner#start_cron_job_thread`; startup is no longer O(N) seconds
178
+ - Remove dead code in `CronRunner`: duplicate `start_delay ||= 0` and always-true `unless start_delay.nil?` guard
179
+ - Make `application.json` path configurable via `MACAW_CONFIG` env var; rescue `Errno::ENOENT` and `JSON::ParserError` with graceful fallback
180
+ - Remove broken SSL2 and SSL3 from `SupportedSSLVersions` (POODLE/DROWN, unavailable on modern OpenSSL)
181
+ - `LoggingAspect` now logs request params, body, and response status on every endpoint call
182
+ - Use `Process.clock_gettime(Process::CLOCK_MONOTONIC)` in `PrometheusAspect` instead of `Time.now` for accurate duration measurement
183
+ - Fix Prometheus `/metrics` endpoint `Content-Type` to `text/plain; version=0.0.4; charset=utf-8`
184
+ - Fix RFC 7230 non-compliance: remove stray space before `\r\n` in HTTP status line
185
+ - Add eviction thread error rescue in `MemoryInvalidationMiddleware` to prevent silent thread death
186
+ - Set `frozen_string_literal: true` consistently across all library files
187
+ - Implement HTTP keep-alive: persistent TCP connections now reuse the same socket for multiple requests without a new handshake
188
+ - Add configurable per-connection read timeout (default 30 s, configurable via `keep_alive_timeout` in `application.json`) to prevent idle or faulty clients from holding worker threads indefinitely
189
+ - Server now responds with `Connection: keep-alive` or `Connection: close` headers according to the client's request
190
+ - Fix request parser to properly detect client EOF and raise `EOFError` instead of propagating `nil` through the parsing pipeline
191
+
192
+ ## [1.5.0] - 2026-03-04
193
+ - Use `IO#timeout=` (Ruby 3.2+) for socket timeout with `SO_RCVTIMEO` fallback for older Ruby and SSL sockets
194
+ - Remove Prometheus integration: `PrometheusAspect`, `PrometheusMiddleware`, and `prometheus-client` gem dependency removed
195
+ - Remove built-in session management: `declare_client_session`, `set_session`, `@session`, and all related configuration removed
196
+ - Remove `CronRunner` and `setup_job`: periodic job scheduling is no longer a responsibility of the framework; use a dedicated job library instead
197
+ - Remove `start_without_server!` method, which existed solely to support cron-only deployments
198
+ - Handle SIGTERM for graceful shutdown in containerised deployments; SIGTERM now triggers the same shutdown path as SIGINT
199
+ - Add `Content-Length: 0` to inline 404 and 500 error responses for RFC 7230 compliance
200
+ - Change default `bind` from `'localhost'` to `'0.0.0.0'` so the server is reachable in containers without explicit configuration
201
+ - Sanitize `\r` and `\n` from response header keys and values to prevent HTTP response splitting attacks
202
+ - Add configurable request body size limit (`max_body_size` in `application.json`, default 1 MB); requests exceeding the limit are rejected with 413 Content Too Large before the body is read
203
+ - Fix `maintain_worker_pool` iteration bug: `each_with_index` + `delete_at` skipped elements after a deletion; replaced with `reject!` + bulk respawn
204
+ - Fix cache TTL fallback: `nil.to_i` returned 0 when `cache_invalidation` was absent, silently creating a zero-TTL cache; replaced with safe navigation `&.to_i || 3_600`
205
+ - Fix `CacheAspect` crash when endpoint returns `nil`: guard added before `response[1]` access
206
+ - Replace `sanitize_parameter_value` character stripping with proper CGI URL-decoding (preserves emails, UUIDs, decimal values)
207
+ - Broaden HTTP version pattern in request parser from `HTTP/1.1` literal to regex — HTTP/1.0 requests now route correctly
208
+ - Add `rescue StandardError` guard in `Cache#invalidation_process` to prevent silent background eviction-thread death
209
+ - Fix `ThreadServer#shutdown` poison-pill count: uses actual `@workers.size` instead of `@num_threads`
210
+ - Add C extension scaffold (`ext/macaw_framework_ext/`) as a foundation for future native performance-sensitive routines
data/Gemfile CHANGED
@@ -6,7 +6,6 @@ gemspec
6
6
 
7
7
  gem 'logger', '~> 1.7'
8
8
  gem 'openssl'
9
- gem 'prometheus-client', '~> 4.1'
10
9
 
11
10
  group :test do
12
11
  gem 'minitest', '~> 5.0'
data/README.md CHANGED
@@ -2,8 +2,7 @@
2
2
  # MacawFramework
3
3
 
4
4
  MacawFramework is a lightweight, easy-to-use web framework for Ruby designed to simplify the development of small to
5
- medium-sized web applications. Weighting less than 26Kb with support for various HTTP methods, caching, and session management,
6
- MacawFramework provides developers with the essential tools to quickly build and deploy their applications.
5
+ medium-sized web applications. Weighing less than 26Kb with support for various HTTP methods and response caching,
7
6
 
8
7
  - [MacawFramework](#macawframework)
9
8
  * [Features](#features)
@@ -14,11 +13,8 @@ MacawFramework provides developers with the essential tools to quickly build and
14
13
  * [Usage](#usage)
15
14
  + [Basic routing: Define routes with support for GET, POST, PUT, PATCH, and DELETE HTTP methods](#basic-routing-define-routes-with-support-for-get-post-put-patch-and-delete-http-methods)
16
15
  + [Caching: Improve performance by caching responses and configuring cache invalidation](#caching-improve-performance-by-caching-responses-and-configuring-cache-invalidation)
17
- + [Session management: Handle user sessions securely with server-side in-memory storage](#session-management-handle-user-sessions-securely-with-server-side-in-memory-storage)
18
- + [Configuration: Customize various aspects of the framework through the application.json configuration file, such as rate limiting, SSL support, and Prometheus integration](#configuration-customize-various-aspects-of-the-framework-through-the-applicationjson-configuration-file-such-as-rate-limiting-ssl-support-and-prometheus-integration)
19
- + [Monitoring: Easily monitor your application performance and metrics with built-in Prometheus support](#monitoring-easily-monitor-your-application-performance-and-metrics-with-built-in-prometheus-support)
16
+ + [Configuration: Customize various aspects of the framework through the application.json configuration file](#configuration-customize-various-aspects-of-the-framework-through-the-applicationjson-configuration-file)
20
17
  + [Routing for "public" Folder: Serve Static Assets](#routing-for-public-folder-serve-static-assets)
21
- + [Periodic Jobs](#periodic-jobs)
22
18
  + [Tips](#tips)
23
19
  * [Contributing](#contributing)
24
20
  * [License](#license)
@@ -27,10 +23,8 @@ MacawFramework provides developers with the essential tools to quickly build and
27
23
  ## Features
28
24
 
29
25
  - Simple routing with support for GET, POST, PUT, PATCH, and DELETE HTTP methods
30
- - Caching middleware for improved performance
31
- - Session management with server-side in-memory storage
32
- - Basic rate limiting and SSL support
33
- - Prometheus integration for monitoring and metrics
26
+ - Response caching middleware for improved performance
27
+ - SSL support
34
28
  - Less than 26Kb
35
29
  - Easy to learn
36
30
 
@@ -52,7 +46,7 @@ We evaluated MacawFramework (Version 1.2.0) to assess its ability to handle simu
52
46
 
53
47
  MacawFramework is built to be highly compatible, since it uses only native Ruby code:
54
48
 
55
- - **MRI**: MacawFramework is compatible with Matz's Ruby Interpreter (MRI), version 3.0.0 and onwards. If you are using this version or a more recent one, you should not encounter any compatibility issues.
49
+ - **MRI**: MacawFramework is compatible with Matz's Ruby Interpreter (MRI), version 3.2.0 and onwards. If you are using this version or a more recent one, you should not encounter any compatibility issues.
56
50
 
57
51
  - **TruffleRuby**: TruffleRuby is another Ruby interpreter that is fully compatible with MacawFramework. This provides developers with more flexibility in their choice of Ruby interpreter.
58
52
 
@@ -93,10 +87,9 @@ end
93
87
 
94
88
  m.post('/submit_data/:path_variable') do |context|
95
89
  context[:body] # Client body data
96
- context[:params] # Client params, like URL parameters or variables
90
+ context[:params] # Client params, like URL parameters or path variables
97
91
  context[:headers] # Client headers
98
92
  context[:params][:path_variable] # The defined path variable can be found in :params
99
- context[:client] # Client session
100
93
  end
101
94
 
102
95
  m.start!
@@ -125,74 +118,30 @@ MacawFramework::Cache.read(:name) # Maria
125
118
 
126
119
  Manual cache does not need any additional configuration.
127
120
 
128
- ### Session management: Handle user sessions with server-side in-memory storage
129
-
130
- Session will only be enabled if it's configurations exists in the `application.json` file.
131
- The session mechanism works by recovering the Session ID from a client sent header. The default
132
- header is `X-Session-ID`, but it can be changed in the `application.json` file.
133
-
134
- This header will be sent back to the user on every response if Session is enabled. Also, the
135
- session ID will be automatically generated and sent to a client if this client does not provide
136
- a session id in the HTTP request. In the case of the client sending an ID of an expired session
137
- the framework will return a new session with a new ID.
138
-
139
- ```ruby
140
- m = MacawFramework::Macaw.new
141
-
142
- m.get('/login') do |context|
143
- # Authenticate user
144
- context[:client][:user_id] = user_id
145
- end
146
-
147
- m.get('/dashboard') do |context|
148
- # Check if the user is logged in
149
- if context[:client][:user_id]
150
- # Show dashboard
151
- else
152
- # Redirect to login
153
- end
154
- end
155
- ```
156
-
157
- ### Configuration: Customize various aspects of the framework through the application.json configuration file, such as rate limiting, SSL support, and Prometheus integration
121
+ ### Configuration: Customize various aspects of the framework through the application.json configuration file
158
122
 
159
123
  ```json
160
124
  {
161
125
  "macaw": {
162
126
  "port": 8080,
163
- "bind": "localhost",
127
+ "bind": "0.0.0.0",
164
128
  "threads": 200,
129
+ "keep_alive_timeout": 30,
130
+ "max_body_size": 1048576,
165
131
  "cache": {
166
132
  "cache_invalidation": 3600
167
133
  },
168
- "prometheus": {
169
- "endpoint": "/metrics"
170
- },
171
- "rate_limiting": {
172
- "window": 10,
173
- "max_requests": 3
174
- },
175
134
  "ssl": {
176
- "min": "SSL3",
135
+ "min": "TLS1.2",
177
136
  "max": "TLS1.3",
178
137
  "key_type": "EC",
179
138
  "cert_file_name": "path/to/cert/file/file.crt",
180
139
  "key_file_name": "path/to/cert/key/file.key"
181
- },
182
- "session": {
183
- "secure_header": "X-Session-ID",
184
- "invalidation_time": 3600
185
140
  }
186
141
  }
187
142
  }
188
143
  ```
189
144
 
190
- ### Monitoring: Easily monitor your application performance and metrics with built-in Prometheus support
191
-
192
- ```shell
193
- curl http://localhost:8080/metrics
194
- ```
195
-
196
145
  ### Routing for "public" Folder: Serve Static Assets
197
146
 
198
147
  MacawFramework allows you to serve static assets, such as CSS, JavaScript, images, etc., through the "public" folder.
@@ -211,29 +160,6 @@ be accessible at http://yourdomain.com/img/logo.png without any additional confi
211
160
 
212
161
  #### Caution: This is incompatible with most non-unix systems, such as Windows. If you are using a non-unix system, you will need to manually configure the "public" folder and use dir as nil to avoid problems.
213
162
 
214
- ### Periodic Jobs
215
-
216
- Macaw Framework supports the declaration of periodic jobs right in your application code. This feature allows developers to
217
- define tasks that run at set intervals, starting after an optional delay. Each job runs in a separate thread, meaning
218
- your periodic jobs can execute in parallel without blocking the rest of your application.
219
-
220
- Here's an example of how to declare a periodic job:
221
-
222
- ```ruby
223
- m = MacawFramework::Macaw.new
224
-
225
- m.setup_job(interval: 5, start_delay: 5, job_name: "cron job 1") do
226
- puts "i'm a periodic job that runs every 5 secs!"
227
- end
228
- ```
229
-
230
- Values for interval and start_delay are in seconds.
231
-
232
- **Caution: Defining a lot of jobs with low interval can severely degrade performance.**
233
-
234
- If you want to build an application with just cron jobs, that don't need to run a web server, you can start
235
- MacawFramework without running a web server with the `start_without_server!` method, instead of `start!`.
236
-
237
163
  ### Tips
238
164
 
239
165
  - The automatic logging and log aspect are now optional. To disable them, simply start Macaw with `custom_log` set to nil.
@@ -271,25 +197,16 @@ m.threads = 300
271
197
 
272
198
  - The default number of virtual threads in the thread pool is 200.
273
199
 
274
- - Rate Limit window should also be specified in seconds. Rate limit will be activated only if the `rate_limiting` config
275
- exists inside `application.json`.
276
-
277
200
  - If the SSL configuration is provided in the `application.json` file with valid certificate and key files, the TCP server
278
201
  will be wrapped with HTTPS security using the provided certificate.
279
202
 
280
- - The supported values for `min` and `max` in the SSL configuration are: `SSL2`, `SSL3`, `TLS1.1`, `TLS1.2`, and `TLS1.3`,
203
+ - The supported values for `min` and `max` in the SSL configuration are: `TLS1.2` and `TLS1.3`,
281
204
  and the supported values for `key_type` are `RSA` and `EC`.
282
205
 
283
- - If Prometheus is enabled, a GET endpoint will be defined at path `/metrics` to collect Prometheus metrics. This path
284
- is configurable via the `application.json` file.
285
-
286
206
  - The verb methods must always return a string or nil (used as the response), a number corresponding to the HTTP status
287
207
  code to be returned to the client, and the response headers as a Hash or nil. If an endpoint doesn't return a value or
288
208
  returns nil for body, status code, and headers, a default 200 OK status will be sent as the response.
289
209
 
290
- - For cron jobs without a start_delay, a value of 0 will be used. For a job without a name, a unique name will be generated
291
- for it.
292
-
293
210
  - Ensure the "public" folder is placed in the same directory as the main.rb file: The "public" folder should contain any static assets,
294
211
  such as CSS, JavaScript, or images, that your web application requires. Placing it in the same directory as the main.rb file ensures
295
212
  that the server can correctly serve these assets.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ append_cflags(%w[-Wall -Wextra -Wno-unused-parameter])
6
+
7
+ create_makefile('macaw_framework_ext')
@@ -0,0 +1,8 @@
1
+ #include "macaw_framework_ext.h"
2
+
3
+ VALUE rb_mMacawFramework;
4
+
5
+ void
6
+ Init_macaw_framework_ext(void) {
7
+ rb_mMacawFramework = rb_define_module("MacawFramework");
8
+ }
@@ -0,0 +1,10 @@
1
+ #ifndef MACAW_FRAMEWORK_EXT_H
2
+ #define MACAW_FRAMEWORK_EXT_H
3
+
4
+ #include "ruby.h"
5
+
6
+ extern VALUE rb_mMacawFramework;
7
+
8
+ void Init_macaw_framework_ext(void);
9
+
10
+ #endif
@@ -4,17 +4,20 @@
4
4
  # Aspect that provide cache for the endpoints.
5
5
  module CacheAspect
6
6
  def call_endpoint(cache, *args, **kwargs)
7
- return super(*args, **kwargs) unless !cache[:cache].nil? && cache[:endpoints_to_cache]&.include?(args[0])
7
+ return super(*args, **kwargs) if cache[:cache].nil? || !cache[:endpoints_to_cache]&.include?(args[0])
8
8
 
9
9
  cache_filtered_name = cache_name_filter(args[1], cache[:cached_methods][args[0]])
10
10
 
11
- cache[:cache].mutex.synchronize do
12
- return cache[:cache].cache[cache_filtered_name][0] unless cache[:cache].cache[cache_filtered_name].nil?
11
+ cached_response = cache[:cache].mutex.synchronize { cache[:cache].cache[cache_filtered_name]&.dig(0) }
12
+ return cached_response unless cached_response.nil?
13
13
 
14
- response = super(*args, **kwargs)
15
- cache[:cache].cache[cache_filtered_name] = [response, Time.now] if should_cache_response?(response[1])
16
- response
14
+ response = super(*args, **kwargs)
15
+ if response && should_cache_response?(response[1])
16
+ cache[:cache].mutex.synchronize do
17
+ cache[:cache].cache[cache_filtered_name] = [response, Time.now]
18
+ end
17
19
  end
20
+ response
18
21
  end
19
22
 
20
23
  private
@@ -1,4 +1,4 @@
1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'logger'
4
4
  require_relative '../data_filters/log_data_filter'
@@ -11,13 +11,22 @@ module LoggingAspect
11
11
  def call_endpoint(logger, *args, **kwargs)
12
12
  return super(*args, **kwargs) if logger.nil?
13
13
 
14
+ endpoint_name = args[1]
15
+ client_data = args[2]
16
+
17
+ logger.info("Calling endpoint: #{endpoint_name} | " \
18
+ "params: #{LogDataFilter.sanitize_for_logging(client_data[:params].to_s)} | " \
19
+ "body: #{LogDataFilter.sanitize_for_logging(client_data[:body])}")
20
+
14
21
  begin
15
- response = super(*args)
22
+ response = super(*args, **kwargs)
16
23
  rescue StandardError => e
17
24
  logger.error("#{e.message}\n#{e.backtrace.join("\n")}")
18
25
  raise e
19
26
  end
20
27
 
28
+ logger.info("Response from endpoint: #{endpoint_name} | status: #{response&.dig(1)}")
29
+
21
30
  response
22
31
  end
23
32
  end
@@ -37,13 +37,8 @@ class MacawFramework::Cache
37
37
  # @example
38
38
  # MacawFramework::Cache.write("name", "Maria", expires_in: 7200)
39
39
  def write(tag, value, expires_in: 3600)
40
- if read(tag).nil?
41
- @mutex.synchronize do
42
- @cache.store(tag, { value: value, expires_in: Time.now + expires_in })
43
- end
44
- else
45
- @cache[tag][:value] = value
46
- @cache[tag][:expires_in] = Time.now + expires_in
40
+ @mutex.synchronize do
41
+ @cache.store(tag, { value: value, expires_in: Time.now + expires_in })
47
42
  end
48
43
  end
49
44
 
@@ -65,7 +60,9 @@ class MacawFramework::Cache
65
60
  #
66
61
  # @example
67
62
  # MacawFramework::Cache.read("name") # Maria
68
- def read(tag) = @cache.dig(tag, :value)
63
+ def read(tag)
64
+ @mutex.synchronize { @cache.dig(tag, :value) }
65
+ end
69
66
 
70
67
  private
71
68
 
@@ -86,6 +83,8 @@ class MacawFramework::Cache
86
83
  @mutex.synchronize do
87
84
  @cache.delete_if { |_, v| v[:expires_in] < Time.now }
88
85
  end
86
+ rescue StandardError => e
87
+ warn "Cache invalidation error: #{e.message}"
89
88
  end
90
89
  end
91
90
  end
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../../middlewares/memory_invalidation_middleware'
4
- require_relative '../../middlewares/rate_limiter_middleware'
5
4
  require_relative '../../data_filters/response_data_filter'
6
- require_relative '../../errors/too_many_requests_error'
7
5
  require_relative '../../utils/supported_ssl_versions'
8
- require_relative '../../aspects/prometheus_aspect'
9
6
  require_relative '../../aspects/logging_aspect'
10
7
  require_relative '../../aspects/cache_aspect'
11
- require 'securerandom'
12
8
 
13
9
  ##
14
10
  # Base module for Server classes. It contains
@@ -18,18 +14,16 @@ require 'securerandom'
18
14
  module ServerBase
19
15
  prepend CacheAspect
20
16
  prepend LoggingAspect
21
- prepend PrometheusAspect
22
17
 
23
18
  private
24
19
 
25
- def call_endpoint(name, client_data, session_id, _client_ip)
20
+ def call_endpoint(name, client_data)
26
21
  @macaw.send(
27
22
  name.to_sym,
28
23
  {
29
24
  headers: client_data[:headers],
30
25
  body: client_data[:body],
31
- params: client_data[:params],
32
- client: @session&.dig(session_id)&.dig(0)
26
+ params: client_data[:params]
33
27
  }
34
28
  )
35
29
  end
@@ -39,30 +33,29 @@ module ServerBase
39
33
  end
40
34
 
41
35
  def handle_client(client)
42
- _path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
43
- raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
44
- raise TooManyRequestsError unless @rate_limit.nil? || @rate_limit.allow?(client.peeraddr[3])
45
-
46
- client_data = get_client_data(body, headers, parameters)
47
- session_id = declare_client_session(client_data[:headers], @macaw.secure_header) if @macaw.session
48
-
49
- message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
50
- method_name, client_data, session_id, client.peeraddr[3])
51
- response_headers ||= {}
52
- response_headers[@macaw.secure_header] = session_id if @macaw.session
53
- status ||= 200
54
- message ||= nil
55
- response_headers ||= nil
56
- client.puts ResponseDataFilter.mount_response(status, response_headers, message)
57
- rescue IOError, Errno::EPIPE => e
58
- @macaw_log&.error("Error writing to client: #{e.message}")
59
- rescue TooManyRequestsError
60
- client.print "HTTP/1.1 429 Too Many Requests\r\n\r\n"
61
- rescue EndpointNotMappedError
62
- client.print "HTTP/1.1 404 Not Found\r\n\r\n"
63
- rescue StandardError => e
64
- client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
65
- @macaw_log&.error(e.full_message)
36
+ apply_socket_timeout(client)
37
+ loop do
38
+ max_body = @macaw.max_body_size || 1_048_576
39
+ _path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes,
40
+ max_body)
41
+ raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
42
+
43
+ keep_alive = keep_alive_connection?(headers)
44
+ client.write build_response(method_name, headers, body, parameters, keep_alive)
45
+ break unless keep_alive
46
+ rescue Errno::ECONNRESET, Errno::EPIPE, IOError
47
+ break
48
+ rescue EndpointNotMappedError
49
+ client.print "HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
50
+ break
51
+ rescue PayloadTooLargeError
52
+ client.print "HTTP/1.1 413 Content Too Large\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
53
+ break
54
+ rescue StandardError => e
55
+ client.print "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
56
+ @macaw_log&.error(e.full_message)
57
+ break
58
+ end
66
59
  ensure
67
60
  begin
68
61
  client.close
@@ -71,20 +64,29 @@ module ServerBase
71
64
  end
72
65
  end
73
66
 
74
- def declare_client_session(headers, secure_header_name)
75
- session_id = headers[secure_header_name] || SecureRandom.uuid
76
- session_id = SecureRandom.uuid if @session[session_id].nil?
77
- @session[session_id] ||= [{}, Time.now]
78
- session_id
67
+ def build_response(method_name, headers, body, parameters, keep_alive)
68
+ client_data = get_client_data(body, headers, parameters)
69
+ message, status, response_headers = call_endpoint(@macaw_log, @cache, method_name, client_data)
70
+ response_headers ||= {}
71
+ status ||= 200
72
+ response_headers['Connection'] = keep_alive ? 'keep-alive' : 'close'
73
+ response_headers['Content-Length'] = message.to_s.bytesize
74
+ ResponseDataFilter.mount_response(status, response_headers, message)
79
75
  end
80
76
 
81
- def set_rate_limiting
82
- return unless @macaw.config&.dig('macaw', 'rate_limiting')
77
+ def apply_socket_timeout(client)
78
+ timeout = @macaw.keep_alive_timeout || 30
79
+ client.timeout = timeout
80
+ rescue NoMethodError
81
+ io = client.respond_to?(:to_io) ? client.to_io : client
82
+ timeval = [timeout, 0].pack('l_2')
83
+ io.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeval)
84
+ rescue StandardError
85
+ nil
86
+ end
83
87
 
84
- @rate_limit = RateLimiterMiddleware.new(
85
- @macaw.config['macaw']['rate_limiting']['window'].to_i || 1,
86
- @macaw.config['macaw']['rate_limiting']['max_requests'].to_i || 60
87
- )
88
+ def keep_alive_connection?(headers)
89
+ headers['Connection']&.downcase != 'close'
88
90
  end
89
91
 
90
92
  def set_ssl
@@ -113,22 +115,8 @@ module ServerBase
113
115
  raise e
114
116
  end
115
117
 
116
- def set_session
117
- return unless @macaw.session
118
-
119
- @session ||= {}
120
- inv = if @macaw.config&.dig('macaw', 'session', 'invalidation_time')
121
- MemoryInvalidationMiddleware.new(@macaw.config['macaw']['session']['invalidation_time'])
122
- else
123
- MemoryInvalidationMiddleware.new
124
- end
125
- inv.cache = @session
126
- end
127
-
128
118
  def set_features
129
119
  @is_shutting_down = false
130
- set_rate_limiting
131
- set_session
132
120
  set_ssl
133
121
  end
134
122
  end
@@ -13,19 +13,13 @@ class ThreadServer
13
13
 
14
14
  attr_reader :context
15
15
 
16
- # rubocop:disable Metrics/ParameterLists
17
-
18
16
  ##
19
17
  # Create a new instance of ThreadServer.
20
18
  # @param {Macaw} macaw
21
- # @param {Logger} logger
22
- # @param {Integer} port
23
- # @param {String} bind
24
- # @param {Integer} num_threads
19
+ # @param {Array} endpoints_to_cache
25
20
  # @param {MemoryInvalidationMiddleware} cache
26
- # @param {Prometheus::Client:Registry} prometheus
27
21
  # @return {Server}
28
- def initialize(macaw, endpoints_to_cache = nil, cache = nil, prometheus = nil, prometheus_mw = nil)
22
+ def initialize(macaw, endpoints_to_cache = nil, cache = nil)
29
23
  @port = macaw.port
30
24
  @bind = macaw.bind
31
25
  @macaw = macaw
@@ -33,19 +27,14 @@ class ThreadServer
33
27
  @num_threads = macaw.threads
34
28
  @work_queue = Queue.new
35
29
  set_features
36
- @rate_limit ||= nil
37
30
  @cache = {
38
31
  cache: cache,
39
32
  endpoints_to_cache: endpoints_to_cache || [],
40
33
  cached_methods: macaw.cached_methods
41
34
  }
42
- @prometheus = prometheus
43
- @prometheus_middleware = prometheus_mw
44
35
  @workers = []
45
36
  end
46
37
 
47
- # rubocop:enable Metrics/ParameterLists
48
-
49
38
  ##
50
39
  # Start running the webserver.
51
40
  def run
@@ -82,40 +71,40 @@ class ThreadServer
82
71
  sleep 0.1
83
72
  end
84
73
 
85
- @num_threads.times { @work_queue << :shutdown }
74
+ worker_count = @workers_mutex.synchronize { @workers.size }
75
+ worker_count.times { @work_queue << :shutdown }
86
76
  @workers.each(&:join)
87
- @server.close
77
+ @server&.close
88
78
  end
89
79
 
90
80
  private
91
81
 
92
82
  def spawn_worker
93
- @workers_mutex.synchronize do
94
- t = Thread.new do
95
- loop do
96
- client = @work_queue.pop
97
- break if client == :shutdown
83
+ @workers_mutex.synchronize { create_worker }
84
+ end
98
85
 
99
- handle_client(client)
100
- end
86
+ def create_worker
87
+ t = Thread.new do
88
+ loop do
89
+ client = @work_queue.pop
90
+ break if client == :shutdown
91
+
92
+ handle_client(client)
101
93
  end
102
- @workers << t
103
- t
104
94
  end
95
+ @workers << t
96
+ t
105
97
  end
106
98
 
107
99
  def maintain_worker_pool
108
100
  @workers_mutex.synchronize do
109
- @workers.each_with_index do |worker, index|
110
- unless worker.alive?
111
- if @is_shutting_down
112
- @macaw_log&.info("Worker thread #{index} finished, not respawning due to server shutdown.")
113
- else
114
- @macaw_log&.error("Worker thread #{index} died, respawning...")
115
- @workers.delete_at(index)
116
- spawn_worker
117
- end
118
- end
101
+ return if @is_shutting_down
102
+
103
+ dead_count = @workers.count { |w| !w.alive? }
104
+ @workers.select!(&:alive?)
105
+ dead_count.times do
106
+ @macaw_log&.error('Worker thread died, respawning...')
107
+ create_worker
119
108
  end
120
109
  end
121
110
  end
@@ -1,6 +1,7 @@
1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'digest'
4
5
 
5
6
  ##
6
7
  # Module responsible for sanitizing log data
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi/escape'
3
4
  require_relative '../errors/endpoint_not_mapped_error'
5
+ require_relative '../errors/payload_too_large_error'
4
6
 
5
7
  ##
6
8
  # Module containing methods to filter Strings
@@ -10,14 +12,17 @@ module RequestDataFiltering
10
12
  ##
11
13
  # Method responsible for extracting information
12
14
  # provided by the client like Headers and Body
13
- def self.parse_request_data(client, routes)
14
- path, parameters = extract_url_parameters(client.gets&.gsub('HTTP/1.1', ''))
15
+ def self.parse_request_data(client, routes, max_body_size = 1_048_576)
16
+ first_line = client.gets
17
+ raise EOFError if first_line.nil?
18
+
19
+ path, parameters = extract_url_parameters(first_line.gsub(%r{\s*HTTP/\d+(?:\.\d+)?\s*$}i, ''))
15
20
  parameters = {} if parameters.nil?
16
21
 
17
22
  method_name = sanitize_method_name(path)
18
23
  method_name = select_path(method_name, routes, parameters)
19
24
  body_first_line, headers = extract_headers(client)
20
- body = extract_body(client, body_first_line, headers['Content-Length'].to_i)
25
+ body = extract_body(client, body_first_line, headers['Content-Length'].to_i, max_body_size)
21
26
  [path, method_name, headers, body, parameters]
22
27
  end
23
28
 
@@ -74,8 +79,8 @@ module RequestDataFiltering
74
79
  def self.extract_headers(client)
75
80
  header = client.gets&.delete("\n")&.delete("\r")
76
81
  headers = {}
77
- while header&.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
78
- split_header = header.split(':')
82
+ while header&.match(/\A[^:]+:\s*.+/)
83
+ split_header = header.split(':', 2)
79
84
  headers[split_header[0].strip] = split_header[1].strip
80
85
  header = client.gets&.delete("\n")&.delete("\r")
81
86
  end
@@ -84,7 +89,9 @@ module RequestDataFiltering
84
89
 
85
90
  ##
86
91
  # Method responsible for extracting the body from request
87
- def self.extract_body(client, body_first_line, content_length)
92
+ def self.extract_body(client, body_first_line, content_length, max_body_size = 1_048_576)
93
+ raise PayloadTooLargeError if content_length > max_body_size
94
+
88
95
  body = client&.read(content_length)
89
96
  body_first_line << body.to_s
90
97
  end
@@ -113,9 +120,13 @@ module RequestDataFiltering
113
120
  end
114
121
 
115
122
  ##
116
- # Method responsible for sanitizing the parameter value
123
+ # Method responsible for sanitizing the parameter value.
124
+ # URL-decodes percent-encoded characters (e.g. %40 → @, %20 → space, + → space).
117
125
  def self.sanitize_parameter_value(value)
118
- value&.gsub(/[^\w\s]/, '')
119
- value&.gsub(/\s/, '')
126
+ return nil if value.nil?
127
+
128
+ CGI.unescape(value)
129
+ rescue ArgumentError
130
+ value
120
131
  end
121
132
  end
@@ -12,20 +12,21 @@ module ResponseDataFilter
12
12
  end
13
13
 
14
14
  def self.mount_first_response_line(status, headers)
15
- separator = " \r\n\r\n"
16
- separator = " \r\n" unless headers.nil?
17
-
18
- "HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]}#{separator}"
15
+ reason = HTTP_STATUS_CODE_MAP[status] || 'Unknown'
16
+ separator = headers.nil? ? "\r\n\r\n" : "\r\n"
17
+ "HTTP/1.1 #{status} #{reason}#{separator}"
19
18
  end
20
19
 
21
20
  def self.mount_response_headers(headers)
22
21
  return '' if headers.nil?
23
22
 
24
- response = ''
23
+ response = +''
25
24
  headers.each do |key, value|
26
- response += "#{key}: #{value}\r\n"
25
+ safe_key = key.to_s.gsub(/[\r\n]/, '')
26
+ safe_value = value.to_s.gsub(/[\r\n]/, '')
27
+ response << "#{safe_key}: #{safe_value}\r\n"
27
28
  end
28
- response += "\r\n"
29
+ response << "\r\n"
29
30
  response
30
31
  end
31
32
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Error raised when the request body exceeds the configured max_body_size.
5
+ class PayloadTooLargeError < StandardError; end
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'errors/endpoint_not_mapped_error'
4
- require_relative 'middlewares/prometheus_middleware'
4
+ require_relative 'errors/payload_too_large_error'
5
5
  require_relative 'data_filters/request_data_filtering'
6
6
  require_relative 'middlewares/memory_invalidation_middleware'
7
- require_relative 'core/cron_runner'
8
7
  require_relative 'core/thread_server'
9
8
  require_relative 'version'
10
- require 'prometheus/client'
11
- require 'securerandom'
12
9
  require 'singleton'
13
10
  require 'pathname'
14
11
  require 'logger'
@@ -23,7 +20,7 @@ module MacawFramework; end
23
20
  # Class responsible for creating endpoints and
24
21
  # starting the web server.
25
22
  class MacawFramework::Macaw
26
- attr_reader :routes, :macaw_log, :config, :jobs, :cached_methods, :secure_header, :session
23
+ attr_reader :routes, :macaw_log, :config, :cached_methods, :keep_alive_timeout, :max_body_size
27
24
  attr_accessor :port, :bind, :threads
28
25
 
29
26
  ##
@@ -119,26 +116,6 @@ class MacawFramework::Macaw
119
116
  map_new_endpoint('delete', cache, path, &block)
120
117
  end
121
118
 
122
- ##
123
- # Spawn and start a thread running the defined periodic job.
124
- # @param {Integer} interval
125
- # @param {Integer?} start_delay
126
- # @param {String} job_name
127
- # @param {Proc} block
128
- # @example
129
- #
130
- # macaw = MacawFramework::Macaw.new
131
- # macaw.setup_job(interval: 60, start_delay: 60, job_name: "job 1") do
132
- # puts "I'm a periodic job that runs every minute"
133
- # end
134
- ##
135
- def setup_job(interval: 60, start_delay: 0, job_name: "job_#{SecureRandom.uuid}", &block)
136
- @cron_runner ||= CronRunner.new(self)
137
- @jobs ||= []
138
- @cron_runner.start_cron_job_thread(interval, start_delay, job_name, &block)
139
- @jobs << job_name
140
- end
141
-
142
119
  ##
143
120
  # Starts the web server
144
121
  def start!
@@ -153,7 +130,8 @@ class MacawFramework::Macaw
153
130
  @macaw_log.info("Number of threads: #{@threads}")
154
131
  @macaw_log.info('---------------------------------')
155
132
  end
156
- @server = @server_class.new(self, @endpoints_to_cache, @cache, @prometheus, @prometheus_middleware)
133
+ @server = @server_class.new(self, @endpoints_to_cache, @cache)
134
+ Signal.trap('TERM') { raise Interrupt }
157
135
  server_loop(@server)
158
136
  rescue Interrupt
159
137
  if @macaw_log.nil?
@@ -167,35 +145,19 @@ class MacawFramework::Macaw
167
145
  end
168
146
  end
169
147
 
170
- ##
171
- # This method is intended to start the framework
172
- # without an web server. This can be useful when
173
- # you just want to keep cron jobs running, without
174
- # mapping any HTTP endpoints.
175
- def start_without_server!
176
- @macaw_log.nil? ? puts('Application starting') : @macaw_log.info('Application starting')
177
- loop { sleep(3600) }
178
- rescue Interrupt
179
- @macaw_log.nil? ? puts('Macaw stop flying for some seeds.') : @macaw_log.info('Macaw stop flying for some seeds.')
180
- end
181
-
182
148
  private
183
149
 
184
150
  def setup_default_configs
185
151
  @port ||= 8080
186
- @bind ||= 'localhost'
152
+ @bind ||= '0.0.0.0'
187
153
  @config ||= nil
188
154
  @threads ||= 200
189
155
  @endpoints_to_cache = []
190
- @prometheus ||= nil
191
- @prometheus_middleware ||= nil
192
156
  end
193
157
 
194
158
  def apply_options(custom_log)
195
159
  setup_basic_config(custom_log)
196
- setup_session
197
160
  setup_cache
198
- setup_prometheus
199
161
  rescue StandardError => e
200
162
  @macaw_log&.warn(e.message)
201
163
  end
@@ -203,33 +165,30 @@ class MacawFramework::Macaw
203
165
  def setup_cache
204
166
  return if @config['macaw']['cache'].nil?
205
167
 
206
- @cache = MemoryInvalidationMiddleware.new(@config['macaw']['cache']['cache_invalidation'].to_i || 3_600)
207
- end
208
-
209
- def setup_session
210
- @session = false
211
- return if @config['macaw']['session'].nil?
212
-
213
- @session = true
214
- @secure_header = @config['macaw']['session']['secure_header'] || 'X-Session-ID'
168
+ @cache = MemoryInvalidationMiddleware.new(@config.dig('macaw', 'cache', 'cache_invalidation')&.to_i || 3_600)
215
169
  end
216
170
 
217
171
  def setup_basic_config(custom_log)
218
172
  @routes = []
219
173
  @cached_methods = {}
220
174
  @macaw_log ||= custom_log
221
- @config = JSON.parse(File.read('application.json'))
175
+ config_file = ENV.fetch('MACAW_CONFIG', 'application.json')
176
+ @config = JSON.parse(File.read(config_file))
222
177
  @port = @config['macaw']['port'] || 8080
223
- @bind = @config['macaw']['bind'] || 'localhost'
178
+ @bind = @config['macaw']['bind'] || '0.0.0.0'
224
179
  @threads = @config['macaw']['threads'] || 200
225
- end
226
-
227
- def setup_prometheus
228
- return unless @config['macaw']['prometheus']
229
-
230
- @prometheus = Prometheus::Client::Registry.new
231
- @prometheus_middleware = PrometheusMiddleware.new
232
- @prometheus_middleware&.configure_prometheus(@prometheus, @config, self)
180
+ @keep_alive_timeout = @config['macaw']['keep_alive_timeout'] || 30
181
+ @max_body_size = @config.dig('macaw', 'max_body_size')&.to_i || 1_048_576
182
+ rescue Errno::ENOENT
183
+ @macaw_log&.warn("Config file '#{config_file}' not found, using default settings.")
184
+ @config = { 'macaw' => {} }
185
+ @keep_alive_timeout = 30
186
+ @max_body_size = 1_048_576
187
+ rescue JSON::ParserError => e
188
+ @macaw_log&.warn("Config file '#{config_file}' is not valid JSON: #{e.message}. Using default settings.")
189
+ @config = { 'macaw' => {} }
190
+ @keep_alive_timeout = 30
191
+ @max_body_size = 1_048_576
233
192
  end
234
193
 
235
194
  def server_loop(server)
@@ -17,8 +17,9 @@ class MemoryInvalidationMiddleware
17
17
  @cache.delete(key) if Time.now - value[1] >= inv_time_seconds
18
18
  end
19
19
  end
20
+ rescue StandardError => e
21
+ warn "MemoryInvalidationMiddleware eviction error: #{e.message}"
20
22
  end
21
23
  end
22
- sleep(2)
23
24
  end
24
25
  end
@@ -4,8 +4,6 @@ require 'openssl'
4
4
 
5
5
  module SupportedSSLVersions
6
6
  VERSIONS = {
7
- 'SSL2' => OpenSSL::SSL::SSL2_VERSION,
8
- 'SSL3' => OpenSSL::SSL::SSL3_VERSION,
9
7
  'TLS1.1' => OpenSSL::SSL::TLS1_1_VERSION,
10
8
  'TLS1.2' => OpenSSL::SSL::TLS1_2_VERSION,
11
9
  'TLS1.3' => OpenSSL::SSL::TLS1_3_VERSION
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = '1.4.1'
4
+ VERSION = '1.5.0'
5
5
  end
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Load the C extension when available (built via `rake compile` or `gem install`).
4
+ # Falls back transparently to pure-Ruby mode if the extension has not been compiled.
5
+ begin
6
+ require 'macaw_framework_ext'
7
+ rescue LoadError
8
+ nil
9
+ end
10
+
3
11
  ##
4
12
  # Main module for all Macaw classes
5
13
  module MacawFramework; end
metadata CHANGED
@@ -1,35 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: macaw_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aria Diniz
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: prometheus-client
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '4.1'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '4.1'
11
+ dependencies: []
26
12
  description: |-
27
13
  A lightweight web framework designed for building efficient backend applications. Initially
28
14
  created for study purposes, now production-ready and open for contributions.
29
15
  email:
30
16
  - aria.diniz.dev@gmail.com
31
17
  executables: []
32
- extensions: []
18
+ extensions:
19
+ - ext/macaw_framework_ext/extconf.rb
33
20
  extra_rdoc_files: []
34
21
  files:
35
22
  - ".rubocop.yml"
@@ -41,23 +28,22 @@ files:
41
28
  - README.md
42
29
  - Rakefile
43
30
  - SECURITY.md
31
+ - ext/macaw_framework_ext/extconf.rb
32
+ - ext/macaw_framework_ext/macaw_framework_ext.c
33
+ - ext/macaw_framework_ext/macaw_framework_ext.h
44
34
  - lib/macaw_framework.rb
45
35
  - lib/macaw_framework/aspects/cache_aspect.rb
46
36
  - lib/macaw_framework/aspects/logging_aspect.rb
47
- - lib/macaw_framework/aspects/prometheus_aspect.rb
48
37
  - lib/macaw_framework/cache.rb
49
38
  - lib/macaw_framework/core/common/server_base.rb
50
- - lib/macaw_framework/core/cron_runner.rb
51
39
  - lib/macaw_framework/core/thread_server.rb
52
40
  - lib/macaw_framework/data_filters/log_data_filter.rb
53
41
  - lib/macaw_framework/data_filters/request_data_filtering.rb
54
42
  - lib/macaw_framework/data_filters/response_data_filter.rb
55
43
  - lib/macaw_framework/errors/endpoint_not_mapped_error.rb
56
- - lib/macaw_framework/errors/too_many_requests_error.rb
44
+ - lib/macaw_framework/errors/payload_too_large_error.rb
57
45
  - lib/macaw_framework/macaw.rb
58
46
  - lib/macaw_framework/middlewares/memory_invalidation_middleware.rb
59
- - lib/macaw_framework/middlewares/prometheus_middleware.rb
60
- - lib/macaw_framework/middlewares/rate_limiter_middleware.rb
61
47
  - lib/macaw_framework/utils/http_status_code.rb
62
48
  - lib/macaw_framework/utils/supported_ssl_versions.rb
63
49
  - lib/macaw_framework/version.rb
@@ -77,6 +63,7 @@ metadata:
77
63
  documentation_uri: https://rubydoc.info/gems/macaw_framework
78
64
  homepage_uri: https://github.com/ariasdiniz/macaw_framework
79
65
  source_code_uri: https://github.com/ariasdiniz/macaw_framework
66
+ changelog_uri: https://github.com/ariasdiniz/macaw_framework/blob/main/CHANGELOG.md
80
67
  rdoc_options: []
81
68
  require_paths:
82
69
  - lib
@@ -84,7 +71,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
84
71
  requirements:
85
72
  - - ">="
86
73
  - !ruby/object:Gem::Version
87
- version: 3.0.0
74
+ version: 3.2.0
88
75
  required_rubygems_version: !ruby/object:Gem::Requirement
89
76
  requirements:
90
77
  - - ">="
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ##
4
- # Aspect that provides application metrics using prometheus.
5
- module PrometheusAspect
6
- def call_endpoint(prometheus_middleware, *args, **kwargs)
7
- return super(*args, **kwargs) if prometheus_middleware.nil?
8
-
9
- start_time = Time.now
10
-
11
- begin
12
- response = super(*args)
13
- ensure
14
- duration = (Time.now - start_time) * 1_000
15
-
16
- endpoint_name = args[2].split('.').join('/')
17
-
18
- prometheus_middleware.request_duration_milliseconds.with_labels(endpoint: endpoint_name).observe(duration)
19
- prometheus_middleware.request_count.with_labels(endpoint: endpoint_name).increment
20
- if response
21
- prometheus_middleware.response_count.with_labels(endpoint: endpoint_name,
22
- status: response[1]).increment
23
- end
24
- end
25
-
26
- response
27
- end
28
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ##
4
- # This module is responsible to set up a new thread
5
- # for each cron job defined
6
- class CronRunner
7
- def initialize(macaw)
8
- @logger = macaw.macaw_log
9
- @macaw = macaw
10
- end
11
-
12
- ##
13
- # Will start a thread for the defined cron job
14
- # @param {Integer} interval
15
- # @param {Integer?} start_delay
16
- # @param {String} job_name
17
- # @param {Proc} block
18
- def start_cron_job_thread(interval, start_delay, job_name, &block)
19
- start_delay ||= 0
20
- raise "interval can't be <= 0 and start_delay can't be < 0!" if interval <= 0 || start_delay.negative?
21
-
22
- @logger&.info("Starting thread for job #{job_name}")
23
- start_delay ||= 0
24
- thread = Thread.new do
25
- name = job_name
26
- interval_thread = interval
27
- unless start_delay.nil?
28
- @logger&.info("Job #{name} scheduled with delay. Will start running in #{start_delay} seconds.")
29
- sleep(start_delay)
30
- end
31
-
32
- loop do
33
- start_time = Time.now
34
- @logger&.info("Running job #{name}")
35
- block.call
36
- @logger&.info("Job #{name} executed with success. New execution in #{interval_thread} seconds.")
37
-
38
- execution_time = Time.now - start_time
39
- sleep_time = [interval_thread - execution_time, 0].max
40
- sleep(sleep_time)
41
- rescue StandardError => e
42
- @logger&.error("Error executing cron job with name #{name}: #{e.message}")
43
- sleep(interval)
44
- end
45
- end
46
- sleep(1)
47
- @logger&.info("Thread for job #{job_name} started")
48
- thread
49
- end
50
- end
@@ -1,4 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class TooManyRequestsError < StandardError
4
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'prometheus/client'
4
- require 'prometheus/client/formats/text'
5
-
6
- ##
7
- # Middleware responsible to configure prometheus
8
- # defining metrics and an endpoint to access them.
9
- class PrometheusMiddleware
10
- attr_accessor :request_duration_milliseconds, :request_count, :response_count
11
-
12
- def configure_prometheus(prometheus_registry, configurations, macaw)
13
- return nil unless prometheus_registry
14
-
15
- @request_duration_milliseconds = Prometheus::Client::Histogram.new(
16
- :request_duration_milliseconds,
17
- docstring: 'The duration of each request in milliseconds',
18
- labels: [:endpoint],
19
- buckets: (100..1000).step(100).to_a + (2000..10_000).step(1000).to_a
20
- )
21
-
22
- @request_count = Prometheus::Client::Counter.new(
23
- :request_count,
24
- docstring: 'The total number of requests received',
25
- labels: [:endpoint]
26
- )
27
-
28
- @response_count = Prometheus::Client::Counter.new(
29
- :response_count,
30
- docstring: 'The total number of responses sent',
31
- labels: %i[endpoint status]
32
- )
33
-
34
- prometheus_registry.register(@request_duration_milliseconds)
35
- prometheus_registry.register(@request_count)
36
- prometheus_registry.register(@response_count)
37
- prometheus_endpoint(prometheus_registry, configurations, macaw)
38
- end
39
-
40
- private
41
-
42
- def prometheus_endpoint(prometheus_registry, configurations, macaw)
43
- endpoint = configurations['macaw']['prometheus']['endpoint'] || '/metrics'
44
- macaw.get(endpoint) do |_context|
45
- [Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200, { 'Content-Type' => 'plaintext' }]
46
- end
47
- end
48
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ##
4
- # Middleware responsible for implementing
5
- # rate limiting
6
- class RateLimiterMiddleware
7
- attr_reader :window_size, :max_requests
8
-
9
- def initialize(window_size, max_requests)
10
- @window_size = window_size
11
- @max_requests = max_requests
12
- @client_timestamps = Hash.new { |key, value| key[value] = [] }
13
- @mutex = Mutex.new
14
- end
15
-
16
- def allow?(client_id)
17
- @mutex.synchronize do
18
- now = Time.now.to_i
19
- timestamps = @client_timestamps[client_id]
20
-
21
- timestamps.reject! { |timestamp| timestamp <= now - window_size }
22
-
23
- if timestamps.length < max_requests
24
- timestamps << now
25
- true
26
- else
27
- false
28
- end
29
- end
30
- end
31
- end