agoo 2.15.10 → 2.15.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +19 -3
- data/ext/agoo/page.c +505 -505
- data/ext/agoo/rserver.c +24 -15
- data/lib/agoo/version.rb +1 -1
- data/misc/flymd.md +581 -0
- data/misc/glue-diagram.svg +3 -0
- data/misc/glue.md +25 -0
- data/misc/optimize.md +100 -0
- data/misc/push.md +581 -0
- data/misc/rails.md +174 -0
- data/misc/song.md +268 -0
- metadata +19 -3
data/misc/rails.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# Rails with Agoo
|
2
|
+
|
3
|
+
Agoo gives Rails a performance boost. It's easy to use Agoo with Rails. Start
|
4
|
+
with a Rails project. Something simple.
|
5
|
+
|
6
|
+
```
|
7
|
+
$ rails new blog
|
8
|
+
$ rails g scaffold User
|
9
|
+
$ rails db:migrate
|
10
|
+
```
|
11
|
+
|
12
|
+
Now run it with Agoo.
|
13
|
+
|
14
|
+
```
|
15
|
+
$ rackup -r agoo -s agoo
|
16
|
+
```
|
17
|
+
|
18
|
+
Or for an extra boost on a machine with multiple cores fire up Agoo with
|
19
|
+
multiple workers. The optimium seems to be about one worker per hyperthread or
|
20
|
+
two per core.
|
21
|
+
|
22
|
+
```
|
23
|
+
$ rackup -r agoo -s agoo -O wc=12
|
24
|
+
```
|
25
|
+
|
26
|
+
What you should see is a faster Rails. Requests to something like
|
27
|
+
`http://localhost:9292/users/1` should be several times faster and asset
|
28
|
+
fetches should be 8000 times faster. Here are some benchmark runs using the
|
29
|
+
[perfer](https://github.com/ohler55/perfer) benchmark tool.
|
30
|
+
|
31
|
+
It is easy to push Rails too hard and watch the latency climb. To avoid that
|
32
|
+
the number of open connections use is adjusted according to how well the
|
33
|
+
server and Rails can handle the load. A balance where the latency is as close
|
34
|
+
to the best attainable is used and the throughput is maximized for that
|
35
|
+
latency is used in all cases.
|
36
|
+
|
37
|
+
All benchmarks were run on a 6 core i7-8700 system with 16GB of memory. The
|
38
|
+
driver, `perfer` was run on a separate machine to allow Rails and the web
|
39
|
+
server full use of the benchmark machine.
|
40
|
+
|
41
|
+
### With Rails managed objects
|
42
|
+
|
43
|
+
For the benchmarks a blank `User` object is created using a browser. The a
|
44
|
+
fetch is performed repeatedly on that object.
|
45
|
+
|
46
|
+
##### With Rails and the default Puma server.
|
47
|
+
|
48
|
+
```
|
49
|
+
$ perfer -t 1 -c 1 -b 1 192.168.1.11:9292 -p /users/1 -d 10
|
50
|
+
Benchmarks for:
|
51
|
+
URL: 192.168.1.11:9292/users/1
|
52
|
+
Threads: 1
|
53
|
+
Connections/thread: 1
|
54
|
+
Duration: 10.0 seconds
|
55
|
+
Keep-Alive: false
|
56
|
+
Results:
|
57
|
+
Throughput: 41 requests/second
|
58
|
+
Latency: 23.612 +/-5.111 msecs (and stdev)
|
59
|
+
```
|
60
|
+
|
61
|
+
##### Now the same request with Agoo in non-clustered mode.
|
62
|
+
|
63
|
+
```
|
64
|
+
$ perfer -t 2 -k -c 4 -b 1 192.168.1.11:9292 -p /users/1 -d 10
|
65
|
+
Benchmarks for:
|
66
|
+
URL: 192.168.1.11:9292/users/1
|
67
|
+
Threads: 2
|
68
|
+
Connections/thread: 4
|
69
|
+
Duration: 5.0 seconds
|
70
|
+
Keep-Alive: true
|
71
|
+
Results:
|
72
|
+
Throughput: 279 requests/second
|
73
|
+
Latency: 28.608 +/-3.497 msecs (and stdev)
|
74
|
+
```
|
75
|
+
|
76
|
+
Agoo was able to handle 8 concurrent connections and still maintain the
|
77
|
+
latency target of under 30 millisecond. Throughput with Agoo was also 7 times
|
78
|
+
greater. Most of the time spent with both Agoo and Puma is most likely in
|
79
|
+
Rails and Rackup so lets try Agoo in cluster mode with the application is
|
80
|
+
stateless.
|
81
|
+
|
82
|
+
##### Now the same request with Agoo in clustered mode.
|
83
|
+
|
84
|
+
```
|
85
|
+
$ perfer -t 2 -k -c 20 -b 1 192.168.1.11:9292 -p /users/1 -d 10
|
86
|
+
Benchmarks for:
|
87
|
+
URL: 192.168.1.11:9292/users/1
|
88
|
+
Threads: 2
|
89
|
+
Connections/thread: 20
|
90
|
+
Duration: 5.0 seconds
|
91
|
+
Keep-Alive: true
|
92
|
+
Results:
|
93
|
+
Throughput: 1476 requests/second
|
94
|
+
Latency: 27.051 +/-11.107 msecs (and stdev)
|
95
|
+
```
|
96
|
+
|
97
|
+
Another 5x boost in throughput. That make Agoo in cluster mode 36 times faster
|
98
|
+
than the default Puma. In all fairness, Puma can be put into clustered mode as
|
99
|
+
well using a custom configuration file. If anyone would like to provide
|
100
|
+
formula for running Puma at optimum please let me know or create a PR for how
|
101
|
+
to run it more effectively.
|
102
|
+
|
103
|
+
### Fetching static assets
|
104
|
+
|
105
|
+
Rails likes to be in charge and is responsible for serving static assets. Some
|
106
|
+
servers such as Agoo offer an option to bypass Rails handling of static
|
107
|
+
assets. Allowing static assets to be loaded directly means CSS, HTML, images,
|
108
|
+
Javascript, and other static files load quickly so that web sites are more
|
109
|
+
snappy even when more than a handful of users are connected.
|
110
|
+
|
111
|
+
##### Rails with the default Puma server loading a static asset.
|
112
|
+
```
|
113
|
+
$ perfer -t 2 -k -c 1 -b 1 192.168.1.11:9292 -p /robots.txt -d 10
|
114
|
+
Benchmarks for:
|
115
|
+
URL: 192.168.1.11:9292/robots.txt
|
116
|
+
Threads: 2
|
117
|
+
Connections/thread: 1
|
118
|
+
Duration: 10.0 seconds
|
119
|
+
Keep-Alive: true
|
120
|
+
Results:
|
121
|
+
Throughput: 79 requests/second
|
122
|
+
Latency: 25.027 +/-7.800 msecs (and stdev)
|
123
|
+
```
|
124
|
+
|
125
|
+
Well, Rails with Puma handles almost twice as as many request as it does for
|
126
|
+
Ruby processing of managed object calls. Note that Puma was able to handle 2
|
127
|
+
concurrent connections without degradation of the latency.
|
128
|
+
|
129
|
+
##### Rails with Agoo loading a static asset in non-clustered mode.
|
130
|
+
```
|
131
|
+
$ perfer -t 2 -k -c 40 -b 2 192.168.1.11:9292 -p /robots.txt -d 10
|
132
|
+
Benchmarks for:
|
133
|
+
URL: 192.168.1.11:9292/robots.txt
|
134
|
+
Threads: 2
|
135
|
+
Connections/thread: 40
|
136
|
+
Duration: 5.0 seconds
|
137
|
+
Keep-Alive: true
|
138
|
+
Results:
|
139
|
+
Throughput: 500527 requests/second
|
140
|
+
Latency: 0.292 +/-0.061 msecs (and stdev)
|
141
|
+
```
|
142
|
+
|
143
|
+
Wow, more than 6000 times faster and the latency drops from 25 milliseconds to
|
144
|
+
a fraction of a millisecond. There is a reason for that. Agoo looks at the
|
145
|
+
`Rails.configuration.assets.paths` variable and sets up a bypass to load those
|
146
|
+
files directly using the same approach as Rails. Ruby is no longer has to deal
|
147
|
+
with basic file serving but can be relogated to taking care of business. It
|
148
|
+
also means tha no Ruby objects are created for serving static assets.
|
149
|
+
|
150
|
+
Now how about Agoo in clustered mode.
|
151
|
+
|
152
|
+
##### Rails with Agoo loading a static asset in clustered mode.
|
153
|
+
```
|
154
|
+
$ perfer -t 2 -k -c 40 -b 2 192.168.1.11:9292 -p /robots.txt -d 10
|
155
|
+
Benchmarks for:
|
156
|
+
URL: 192.168.1.11:9292/robots.txt
|
157
|
+
Threads: 2
|
158
|
+
Connections/thread: 40
|
159
|
+
Duration: 5.0 seconds
|
160
|
+
Keep-Alive: true
|
161
|
+
Results:
|
162
|
+
Throughput: 657777 requests/second
|
163
|
+
Latency: 0.223 +/-0.073 msecs (and stdev)
|
164
|
+
```
|
165
|
+
|
166
|
+
A bit faster but not that much over the non-clustered Agoo. The limiting
|
167
|
+
factor with static assets is the network. Agoo handles 80 concurrent
|
168
|
+
connections at the same latency as one.
|
169
|
+
|
170
|
+
### Summary
|
171
|
+
|
172
|
+
Benchmarks are not everything but if they are an important consideration when
|
173
|
+
selecting a web server for Rails or Rack then Agoo clearly has an advantage
|
174
|
+
over the Rack and Rails default.
|
data/misc/song.md
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
# GraphQL with a Song
|
2
|
+
|
3
|
+
You've may have heard developer singing praises about how wonderful GraphQL
|
4
|
+
is. Maybe you thought it was looking into. I like to learn new technologies by
|
5
|
+
using them so this article is a simple example of using GraphQL.
|
6
|
+
|
7
|
+
GraphQL is a language for describing an applicaiton API. It holds a similar
|
8
|
+
position in the development stack as a REST API but with more
|
9
|
+
flexibility. Unlike REST, GraphQL allows response formats and content to be
|
10
|
+
specified by the client. Just as SQL `SELECT` statements allow query results
|
11
|
+
to be specified, GraphQL allows returned JSON data structure to be
|
12
|
+
specified. Following the SQL analogy GraphQL does not provide a `WHERE` clause
|
13
|
+
but identifies fields on application objects that should provide the data for
|
14
|
+
the response.
|
15
|
+
|
16
|
+
GraphQL, as the name suggests models APIs as if the application is a graph of
|
17
|
+
data. While that description may not be how you have viewed your application
|
18
|
+
it is a model used in most systems. If your application is object based then
|
19
|
+
the objects refer to other objects. These object-method-object define a
|
20
|
+
graph. Data that can be represented by JSON is a graph as JSON is just a
|
21
|
+
directed graph. Thinking about the application as presenting a graph model
|
22
|
+
through the API will make GraphQL much easier to understand.
|
23
|
+
|
24
|
+
## Consider the Application
|
25
|
+
|
26
|
+
Enough of the abstract. Lets get down to actually building an application that
|
27
|
+
uses GraphQL by starting with a definition of the data model or the
|
28
|
+
graph. Last year I picked up a new hobby. I'm learning to play the electric
|
29
|
+
upright bass and about music so a music related example came to mind when
|
30
|
+
coming up with and example.
|
31
|
+
|
32
|
+
Keeping it simple the object types are __Artist__ and __Song__. __Artists__
|
33
|
+
have multiple __Song__ and a __Song__ is associate with an __Artist__. Each
|
34
|
+
object type has attributes such as a `name`. Pretty basic and simple.
|
35
|
+
|
36
|
+
## Define the API
|
37
|
+
|
38
|
+
GraphQL uses SDL (Schema Definition Language) which is sometimes refered to as
|
39
|
+
"type system definition language" in the GraphQL specification. GraphQL types
|
40
|
+
can, in theory be defined in any language but most common language agnostic
|
41
|
+
language is SDL so lets use SDL to define the API.
|
42
|
+
|
43
|
+
```
|
44
|
+
type Artist {
|
45
|
+
name: String!
|
46
|
+
songs: [Song]
|
47
|
+
origin: [String]
|
48
|
+
}
|
49
|
+
|
50
|
+
type Song {
|
51
|
+
name: String!
|
52
|
+
artist: Artist
|
53
|
+
duration: Int
|
54
|
+
release: String
|
55
|
+
}
|
56
|
+
```
|
57
|
+
|
58
|
+
Pretty easy to understand. An __Artist__ has a `name` that is a `String`,
|
59
|
+
`songs` that is an array of __Song__ objects, and `origin` which is a `String`
|
60
|
+
array. __Song__ is similar but with one odd field. The `release` field should
|
61
|
+
be a time or date type but GraphQL does not have that type defined as a core
|
62
|
+
type. To be completely portable between any GraphQL implementation a `String`
|
63
|
+
is used. The GraphQL implemenation we will use as added the `Time` type so
|
64
|
+
lets change the __Song__ definition so that the `release` field is a `Time`
|
65
|
+
type. The returned value will be a `String` but by setting the type to `Time`
|
66
|
+
we document the API more accurately.
|
67
|
+
|
68
|
+
```
|
69
|
+
release: Time
|
70
|
+
```
|
71
|
+
|
72
|
+
The last step is to describe how to get one or more of the objects. This is
|
73
|
+
referred to as the root or for queries the query root. Our root will have just
|
74
|
+
one field or method called `artist` and will require an artist `name`.
|
75
|
+
|
76
|
+
```
|
77
|
+
type Query {
|
78
|
+
artist(name: String!): Artist
|
79
|
+
}
|
80
|
+
```
|
81
|
+
|
82
|
+
## Writing the Application
|
83
|
+
|
84
|
+
Ruby will be used write the application. There is more than one implemenation
|
85
|
+
of a GraphQL server for Ruby. Some approachs require the SDL above to be
|
86
|
+
translated into a Ruby equivalent. [Agoo](https://github.com/ohler55/agoo)
|
87
|
+
uses the SDL defintion as it is and the Ruby code is plain vanilla Ruby so
|
88
|
+
that will be used for this example.
|
89
|
+
|
90
|
+
By having the Ruby class names match the GraphQL type names we keep things
|
91
|
+
simple. Note that the Ruby classes match the GraphQL types.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class Artist
|
95
|
+
attr_reader :name
|
96
|
+
attr_reader :songs
|
97
|
+
attr_reader :origin
|
98
|
+
|
99
|
+
def initialize(name, origin)
|
100
|
+
@name = name
|
101
|
+
@songs = []
|
102
|
+
@origin = origin
|
103
|
+
end
|
104
|
+
|
105
|
+
def song(args={})
|
106
|
+
@songs[args['name']]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Only used by the Song to add itself to the artist.
|
110
|
+
def add_song(song)
|
111
|
+
@songs << song
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Song
|
116
|
+
attr_reader :name # string
|
117
|
+
attr_reader :artist # reference
|
118
|
+
attr_reader :duration # integer
|
119
|
+
attr_reader :release # time
|
120
|
+
|
121
|
+
def initialize(name, artist, duration, release)
|
122
|
+
@name = name
|
123
|
+
@artist = artist
|
124
|
+
@duration = duration
|
125
|
+
@release = release
|
126
|
+
artist.add_song(self)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
Method match fields in the Ruby classes. Note the method all have
|
132
|
+
either no arguments or `args={}`. This is what the GraphQL APIs expect
|
133
|
+
and the [Agoo](https://github.com/ohler55/agoo) GraphQL implementation
|
134
|
+
follows suit. [Agoo](https://github.com/ohler55/agoo) offers
|
135
|
+
additional options that are available based on the method
|
136
|
+
signature. If a method signature is `(args, req)` or `(args, req,
|
137
|
+
plan)` then the request object is included. The plan, if included, is
|
138
|
+
an object that can be queried for information about the query. (It has
|
139
|
+
not been implemented as of version 2.12.0.)
|
140
|
+
|
141
|
+
The `initialize` methods are used to set up the data for this example
|
142
|
+
as we will see shortly.
|
143
|
+
|
144
|
+
The query root class als needs to be defined. Note the `artist` method which
|
145
|
+
matches the SDL `Query` root type. An `attr_reader` for `artists` was also
|
146
|
+
added. That would be exposed to the API simply by adding that field to the
|
147
|
+
`Query` type in the SDL document.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class Query
|
151
|
+
attr_reader :artists
|
152
|
+
|
153
|
+
def initialize(artists)
|
154
|
+
@artists = artists
|
155
|
+
end
|
156
|
+
|
157
|
+
def artist(args={})
|
158
|
+
@artists[args['name']]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
The GraphQL root, not to be confused with the query root sites above the query
|
164
|
+
root. GraphQL defines it to optionally have three field. The Ruby class in
|
165
|
+
this case implements on the `query` field. The initializer loads up some data
|
166
|
+
for an Indie band from New Zealand I like to listen to.
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class Schema
|
170
|
+
attr_reader :query
|
171
|
+
attr_reader :mutation
|
172
|
+
attr_reader :subscription
|
173
|
+
|
174
|
+
def initialize()
|
175
|
+
# Set up some data for testing.
|
176
|
+
artist = Artist.new('Fazerdaze', ['Morningside', 'Auckland', 'New Zealand'])
|
177
|
+
Song.new('Jennifer', artist, 240, Time.utc(2017, 5, 5))
|
178
|
+
Song.new('Lucky Girl', artist, 170, Time.utc(2017, 5, 5))
|
179
|
+
Song.new('Friends', artist, 194, Time.utc(2017, 5, 5))
|
180
|
+
Song.new('Reel', artist, 193, Time.utc(2015, 11, 2))
|
181
|
+
@artists = {artist.name => artist}
|
182
|
+
|
183
|
+
@query = Query.new(@artists)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
The final hookup is implemenation specific. For
|
189
|
+
[Agoo](https://github.com/ohler55/agoo) the server is initialized to include a
|
190
|
+
handler for the `/graphql` HTTP request path ad then it is started.
|
191
|
+
|
192
|
+
```
|
193
|
+
Agoo::Server.init(6464, 'root', thread_count: 1, graphql: '/graphql')
|
194
|
+
Agoo::Server.start()
|
195
|
+
```
|
196
|
+
|
197
|
+
The GraphQL implementation is then configured with the SDL defined earlier
|
198
|
+
(`$songs_sdl`) and then the appliation sleeps while the server processes
|
199
|
+
requests.
|
200
|
+
|
201
|
+
```
|
202
|
+
Agoo::GraphQL.schema(Schema.new) {
|
203
|
+
Agoo::GraphQL.load($songs_sdl)
|
204
|
+
}
|
205
|
+
sleep
|
206
|
+
```
|
207
|
+
|
208
|
+
The code for this example is in (https://github.com/ohler55/agoo/example/graphql/song.rb).
|
209
|
+
|
210
|
+
## Using the API
|
211
|
+
|
212
|
+
A web broswer can be used to try out the API as can `curl`.
|
213
|
+
|
214
|
+
The GraphQL query to try looks like the following.
|
215
|
+
|
216
|
+
```
|
217
|
+
{
|
218
|
+
artist(name:"Fazerdaze") {
|
219
|
+
name
|
220
|
+
songs{
|
221
|
+
name
|
222
|
+
duration
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
```
|
227
|
+
|
228
|
+
The query asks for the __Artist__ named `Frazerdaze` and returns the `name` and `songs` in a JSON document. For each __Song__ the `name` and `duration` of the __Song__ is returned in a JSON object for each __Song__. The output should look like this.
|
229
|
+
|
230
|
+
```
|
231
|
+
{
|
232
|
+
"data":{
|
233
|
+
"artist":{
|
234
|
+
"name":"Fazerdaze",
|
235
|
+
"songs":[
|
236
|
+
{
|
237
|
+
"name":"Jennifer",
|
238
|
+
"duration":240
|
239
|
+
},
|
240
|
+
{
|
241
|
+
"name":"Lucky Girl",
|
242
|
+
"duration":170
|
243
|
+
},
|
244
|
+
{
|
245
|
+
"name":"Friends",
|
246
|
+
"duration":194
|
247
|
+
},
|
248
|
+
{
|
249
|
+
"name":"Reel",
|
250
|
+
"duration":193
|
251
|
+
}
|
252
|
+
]
|
253
|
+
}
|
254
|
+
}
|
255
|
+
}
|
256
|
+
```
|
257
|
+
|
258
|
+
After getting rid of the optional whitespace in the query an HTTP GET made
|
259
|
+
with curl should return that content.
|
260
|
+
|
261
|
+
```
|
262
|
+
curl -w "\n" 'localhost:6464/graphql?query=\{artist(name:"Fazerdaze")\{name,songs\{name,duration\}\}\}&indent=2'
|
263
|
+
```
|
264
|
+
|
265
|
+
Try changing the query and replace `duration` with `release` and not the
|
266
|
+
conversion of the Ruby Time to a JSON string.
|
267
|
+
|
268
|
+
Hope you enjoyed the example. Now you can sing the GraphQL song.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: agoo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.15.
|
4
|
+
version: 2.15.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Ohler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04
|
11
|
+
date: 2024-06-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -41,6 +41,13 @@ extra_rdoc_files:
|
|
41
41
|
- README.md
|
42
42
|
- CHANGELOG.md
|
43
43
|
- LICENSE
|
44
|
+
- misc/flymd.md
|
45
|
+
- misc/glue.md
|
46
|
+
- misc/optimize.md
|
47
|
+
- misc/push.md
|
48
|
+
- misc/rails.md
|
49
|
+
- misc/song.md
|
50
|
+
- misc/glue-diagram.svg
|
44
51
|
files:
|
45
52
|
- CHANGELOG.md
|
46
53
|
- LICENSE
|
@@ -150,6 +157,13 @@ files:
|
|
150
157
|
- lib/agoo/graphql/type.rb
|
151
158
|
- lib/agoo/version.rb
|
152
159
|
- lib/rack/handler/agoo.rb
|
160
|
+
- misc/flymd.md
|
161
|
+
- misc/glue-diagram.svg
|
162
|
+
- misc/glue.md
|
163
|
+
- misc/optimize.md
|
164
|
+
- misc/push.md
|
165
|
+
- misc/rails.md
|
166
|
+
- misc/song.md
|
153
167
|
- test/base_handler_test.rb
|
154
168
|
- test/bind_test.rb
|
155
169
|
- test/domain_test.rb
|
@@ -164,7 +178,9 @@ homepage: https://github.com/ohler55/agoo
|
|
164
178
|
licenses:
|
165
179
|
- MIT
|
166
180
|
metadata:
|
181
|
+
bug_tracker_uri: https://github.com/ohler55/agoo/issues
|
167
182
|
changelog_uri: https://github.com/ohler55/agoo/CHANGELOG.md
|
183
|
+
documentation_uri: http://www.ohler.com/agoo/index.html
|
168
184
|
source_code_uri: https://github.com/ohler55/agoo
|
169
185
|
homepage: https://github.com/ohler55/agoo
|
170
186
|
post_install_message:
|
@@ -193,7 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
193
209
|
version: '0'
|
194
210
|
requirements:
|
195
211
|
- Linux or macOS
|
196
|
-
rubygems_version: 3.4.
|
212
|
+
rubygems_version: 3.4.1
|
197
213
|
signing_key:
|
198
214
|
specification_version: 4
|
199
215
|
summary: An HTTP server
|