duel-memcached 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4100ff714345815d7406440fc25bab63b6e3079aab787089d3f43f226fb1dd17
4
+ data.tar.gz: 83f9ed2340221e9b109c1eb84df79eec67a9a6b1f6de2c066d599a6753b17b3c
5
+ SHA512:
6
+ metadata.gz: 6d3f2f03e8840190589dd12a95fddc233c776cc5d0bff6d5a9c10af85bd7312bdd4e418d4185f9c668d04febf6c80de1c3c45527f70ea3a565f9479fcbcbc68a
7
+ data.tar.gz: 4477a598c7dafc1340e19c28c8ec68f25932512933fc2fc228b9fa96eec3a0a3105d8e3e6390c0a2b69531867364b8bbbb9244a603bffcbde29ec6df0a22b662
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-24
11
+
12
+ The first release. duel-memcached is a Duel plugin, open source under
13
+ the MIT license. It needs Ruby 3.4 or later, Duel 0.1, and `memcached`
14
+ (1.6 or later) on the `PATH`.
15
+
16
+ ### Added
17
+
18
+ - `c.use :memcached`: a memcached server for each Duel worker, on a
19
+ Unix socket, and the `memcached_operations`, `memcached_misses` and
20
+ `memcached_bytes` metrics (and `memcached_cpu`, with `cpu: true`), from
21
+ its statistics, read over a connection of the plugin's own whose
22
+ traffic isn't counted.
23
+ - `Duel::Memcached.servers` and `Duel::Memcached.flush`, for benchmarks:
24
+ `flush` opens a connection of its own, from the worker's
25
+ `DUEL_MEMCACHED_SERVERS`. It's for `setup` hooks, and `once` and
26
+ `untimed` blocks: while a benchmark is measured (`Duel.measuring?`) it raises `Duel::Error`,
27
+ as the flush would be counted.
28
+ - `c.use :memcached` returns the plugin's own connection, opened in each
29
+ worker (`c.in_worker`) and closed as it ends (`at_stop`). Its
30
+ `value` fails outside a worker, saying so. The module keeps no
31
+ connection.
32
+ - `memcached_cpu` is a `kind: :ratio` measure, left out of Duel's
33
+ across-the-board check (`across: false`), in microseconds
34
+ (`unit: "µs"`), so reports show it as time. `memcached_bytes` is in
35
+ bytes (`unit: "B"`), shown after the number. `memcached_operations`
36
+ and `memcached_misses` are in operations and misses (`unit:`, each
37
+ with its plural), so reports read "1 operation → 2 operations".
38
+ - Its metrics are costly to read (Duel's `costly`): a benchmark with an
39
+ `around_iteration` hook reads the server's statistics only as each
40
+ sample starts and stops, not twice an iteration, and what the hook
41
+ sends the server (a transaction's `BEGIN` and `ROLLBACK`, say) is
42
+ learned in each worker and subtracted.
43
+ - Reading the statistics allocates little: the reply is read whole into
44
+ a buffer the connection reuses (`Connection#stats_reply`), and each
45
+ metric's statistics are found and read in place, digit by digit. On
46
+ memcached 1.6's 94 statistics, `memcached_cpu`'s reading allocates
47
+ nothing, and the counted metrics' allocates only their Hash.
48
+ `Duel::Memcached.sum` takes that reply.
49
+
50
+
51
+ [Unreleased]: https://github.com/sferik/duel-ruby/compare/duel-memcached-v0.1.0...HEAD
52
+ [0.1.0]: https://github.com/sferik/duel-ruby/releases/tag/duel-memcached-v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erik Berlin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # duel-memcached
2
+
3
+ [![Duel](https://raw.githubusercontent.com/sferik/duel-ruby/main/assets/badge.svg)](https://github.com/sferik/duel-ruby)
4
+ [![duel-memcached](https://github.com/sferik/duel-ruby/actions/workflows/plugin-memcached.yml/badge.svg)](https://github.com/sferik/duel-ruby/actions/workflows/plugin-memcached.yml)
5
+ [![Gem Version](https://badge.fury.io/rb/duel-memcached.svg)](https://rubygems.org/gems/duel-memcached)
6
+
7
+ A [Duel](https://github.com/sferik/duel-ruby) plugin for code that talks
8
+ to memcached: each Duel worker gets a memcached server of its own, and
9
+ every benchmark is judged on what memcached did for it too.
10
+
11
+ | Metric | What it is | A regression |
12
+ |---|---|---|
13
+ | `memcached_operations` | the keys it fetched, stored, touched, deleted, incremented or decremented, and flushes | any increase |
14
+ | `memcached_misses` | the operations that found no key | any increase |
15
+ | `memcached_bytes` | the bytes it read and wrote: requests, replies, keys and values | more than one an iteration |
16
+ | `memcached_cpu` | its CPU time, in µs (only with `cpu: true`) | > 5% slower, 99% confident |
17
+
18
+ So a change that fetches one more key per request, stops finding what it
19
+ cached, or caches bigger values fails even when its CPU time on the Ruby
20
+ side doesn't change:
21
+
22
+ ```
23
+ REGRESSION store: worse memcached_operations, memcached_bytes in every measurement
24
+ memcached_operations: 1 operation → 2 operations an iteration
25
+ memcached_bytes: 34 B → 70 B an iteration
26
+ ```
27
+
28
+ ## Installation
29
+
30
+ ```ruby
31
+ # Gemfile
32
+ group :development do
33
+ gem "duel-memcached"
34
+ end
35
+ ```
36
+
37
+ > [!NOTE]
38
+ > It needs `memcached` (1.6 or later) on the `PATH`.
39
+
40
+ ## Usage
41
+
42
+ ```ruby
43
+ # .duel.rb
44
+ Duel.configure { |c| c.use :memcached }
45
+ ```
46
+
47
+ Each worker's server listens only on a Unix socket in the worker's own
48
+ directory, with 32 MB of memory and one thread. The socket's path is in
49
+ `DUEL_MEMCACHED_SERVERS`, and in `MEMCACHE_SERVERS` too, so a client that
50
+ reads that (Dalli does) finds it and nothing reaches another memcached.
51
+ Benchmarks can call `Duel::Memcached.servers` and
52
+ `Duel::Memcached.flush`:
53
+
54
+ ```ruby
55
+ # duel/cache_duel.rb
56
+ duel "cache: a hit" do
57
+ once do
58
+ Duel::Memcached.flush # empties the worker's server (over a connection of its own)
59
+ @cache = Dalli::Client.new(Duel::Memcached.servers) # its socket
60
+ @cache.set("k", "v")
61
+ end
62
+ @cache.get("k")
63
+ end
64
+ ```
65
+
66
+ > [!IMPORTANT]
67
+ > `Duel::Memcached.flush` is for setting up: call it in a `setup` hook,
68
+ > or a `once` or `untimed` block. While a benchmark is measured (`Duel.measuring?`), it
69
+ > raises `Duel::Error` rather than have the flush counted.
70
+
71
+ Options:
72
+
73
+ ```ruby
74
+ c.use :memcached,
75
+ server: "/opt/memcached/bin/memcached", # the program (default: DUEL_MEMCACHED_SERVER, or memcached)
76
+ args: ["-m", "256"], # more of its options (they win)
77
+ env: %w[CACHE_SERVERS], # variables given its socket besides DUEL_MEMCACHED_SERVERS (default: MEMCACHE_SERVERS)
78
+ cpu: true # judge memcached_cpu too
79
+ ```
80
+
81
+ The metrics come from the server's `stats`, read over a connection of
82
+ the plugin's own, opened as each worker starts (Duel's `c.in_worker`)
83
+ and closed as it ends. `c.use :memcached` returns it, for a `.duel.rb`
84
+ that reads the server too (`connection.value`, in a worker only).
85
+ `Duel::Memcached.flush` opens a connection of its own, from the worker's
86
+ `DUEL_MEMCACHED_SERVERS`: a flush is an operation, and its bytes count
87
+ as any client's (so it fails while a benchmark is measured).
88
+ `memcached_operations` counts keys, not commands: a
89
+ `get_multi` of ten keys is ten. Memcached keeps no count of commands or
90
+ replies, so a change from one `get_multi` to ten `get`s shows in
91
+ `memcached_bytes`, not in operations.
92
+
93
+ `memcached_bytes` leaves out the plugin's own traffic, to the byte (a
94
+ reading's reply is longer when the numbers in it are), so it's exact.
95
+ But what the server makes up (CAS ids) grows a digit now and then, and so
96
+ may values of your code's (times, ids), so a byte more an iteration
97
+ isn't a regression.
98
+
99
+ `memcached_cpu` is off unless asked for: memcached's CPU time (from
100
+ `rusage_user` and `rusage_system`, to the microsecond) is the whole
101
+ process's, background threads and all, and depends on how fast the
102
+ client sends it work. It's noisy, and what it would catch (more work for
103
+ the server) usually shows exactly in operations and bytes first. When
104
+ it's on, it's judged above 300µs a sample (on either version), against
105
+ a limit of 1.05, and not across the board (where Duel judges every
106
+ benchmark's median together, with a margin a quarter of the limit's:
107
+ too fine for it).
108
+
109
+ Duel's `c.measure` changes any of their settings, or leaves one out:
110
+
111
+ ```ruby
112
+ c.measure("memcached_bytes", tolerance: 8) # values whose length varies more
113
+ c.measure("memcached_misses", skip: true)
114
+ ```
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Duel
4
+ module Memcached
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "duel"
4
+ require "socket"
5
+ require_relative "memcached/version"
6
+
7
+ module Duel
8
+ # A memcached server of each worker's own, and what it did for each
9
+ # benchmark:
10
+ #
11
+ # - memcached_operations: the keys it fetched, stored, touched,
12
+ # deleted, incremented or decremented, and flushes (counted)
13
+ # - memcached_misses: the operations that found no key (counted)
14
+ # - memcached_bytes: the bytes it read and wrote, the protocol's and
15
+ # the values' (counted)
16
+ # - memcached_cpu: its CPU time, in microseconds (judged like CPU
17
+ # time; only with cpu: true)
18
+ #
19
+ # # .duel.rb
20
+ # Duel.configure { |c| c.use :memcached }
21
+ #
22
+ # Benchmarks can call Duel::Memcached.servers, and
23
+ # Duel::Memcached.flush in setup (or untimed blocks): while a benchmark
24
+ # is measured, it fails.
25
+ #
26
+ # The server listens only on a Unix socket in the worker's directory,
27
+ # with one thread (so its statistics are up to date when they're read);
28
+ # the socket's path is in DUEL_MEMCACHED_SERVERS, and in
29
+ # MEMCACHE_SERVERS too (unless +env+ names others), so a client that
30
+ # reads that (Dalli does) finds it.
31
+ module Memcached
32
+ Duel.plugin(:memcached, self)
33
+
34
+ SERVERS = "DUEL_MEMCACHED_SERVERS"
35
+
36
+ # Configures Duel for the plugin (c.use :memcached): the server
37
+ # (+server+ is the program, +args+ more of its options) and its
38
+ # metrics, memcached_cpu too with +cpu+. Returns the plugin's own
39
+ # connection to it, a worker's (see Config#in_worker, and in tests,
40
+ # Duel::Testing.in_worker).
41
+ def self.configure(config, server: ENV.fetch("DUEL_MEMCACHED_SERVER", "memcached"), args: [], env: %w[MEMCACHE_SERVERS], cpu: false)
42
+ config.prepare(:worker) do |w|
43
+ socket = w.path("memcached.sock")
44
+ w.spawn(*[server, "-s", socket, "-m", "32", "-t", "1"] + args)
45
+ w.wait_until { Connection.ready?(socket) }
46
+ ([SERVERS] + env).to_h { |name| [name, socket] }
47
+ end
48
+ connection = config.in_worker { |w| Connection.new(servers).tap { |memcached| w.at_stop { memcached.close } } }
49
+ config.metric(METRICS) { stats(connection.value) }.costly
50
+ config.metric("memcached_cpu", **CPU) { cpu_time(connection.value) }.costly if cpu
51
+ connection
52
+ end
53
+
54
+ # (Reading the statistics costs these nothing: stats isn't an
55
+ # operation, and the plugin's own bytes are taken away.)
56
+ METRICS = {
57
+ "memcached_operations" => {kind: :counted, unit: %w[operation operations], calibrate: false},
58
+ "memcached_misses" => {kind: :counted, unit: %w[miss misses], calibrate: false},
59
+ # Values the server makes up (CAS ids, say) grow a digit now and
60
+ # then, and so may the code's own: a byte more an iteration isn't a
61
+ # regression.
62
+ "memcached_bytes" => {kind: :counted, unit: "B", tolerance: 1, calibrate: false}
63
+ }
64
+
65
+ # memcached_cpu's settings. Its CPU time is the whole process's (its
66
+ # background threads too), and swings with how the client paces it:
67
+ # noisy, so judged only above 300µs, against a limit of its own, not
68
+ # across the board (where the margin is a quarter of that), and only
69
+ # when asked for. (A metric of its own: read around every sample, as a
70
+ # ratio one is, where the counted ones are read only in counting
71
+ # samples.)
72
+ CPU = {kind: :ratio, unit: "µs", floor: 300, limit: 1.05, across: false}
73
+
74
+ # The statistics each is the sum of.
75
+ OPERATIONS = %w[cmd_get cmd_set cmd_touch cmd_flush delete_hits delete_misses incr_hits incr_misses decr_hits decr_misses].freeze
76
+ MISSES = %w[get_misses touch_misses delete_misses incr_misses decr_misses cas_misses].freeze
77
+ BYTES = %w[bytes_read bytes_written].freeze
78
+ RUSAGE = %w[rusage_user rusage_system].freeze
79
+
80
+ # What a statistic's line starts with, by its name (made once each).
81
+ NEEDLES = Hash.new { |needles, name| needles[name] = "STAT #{name} " }
82
+
83
+ # What the worker's server has done so far, read over +connection+
84
+ # (the plugin's own): its traffic isn't counted (it's known to the
85
+ # byte, while what readings write varies with the numbers in them).
86
+ # Read around every counting sample, it allocates only its Hash.
87
+ def self.stats(connection)
88
+ stats = connection.stats_reply
89
+ {"memcached_operations" => sum(stats, OPERATIONS),
90
+ "memcached_misses" => sum(stats, MISSES),
91
+ "memcached_bytes" => sum(stats, BYTES) - connection.own}
92
+ end
93
+
94
+ # Its CPU time so far, in microseconds, read over +connection+: the
95
+ # readings' too, as they're too slight to matter and too unsteady to
96
+ # take away. Read around every sample, it allocates nothing.
97
+ def self.cpu_time(connection) = sum(connection.stats_reply, RUSAGE)
98
+
99
+ # The sum of +names+ in +stats+, a stats reply as the server sent it.
100
+ def self.sum(stats, names) = names.sum { |name| value(stats, name) }
101
+
102
+ # The value of +name+ in +stats+ (a stats reply), read in place, digit
103
+ # by digit, so as to allocate nothing: rusage_* are seconds with six
104
+ # decimals, microseconds without the point.
105
+ def self.value(stats, name)
106
+ needle = NEEDLES[name]
107
+ at = stats.byteindex(needle) or raise KeyError, "key not found: #{name.inspect}"
108
+ at += needle.bytesize
109
+ raise ArgumentError, "memcached: #{name} isn't a number" unless stats.match?(/\G\d+(?:\.\d+)?\r/, at)
110
+
111
+ value = 0
112
+ until (byte = stats.getbyte(at)) == 13 # "\r", where the number ends
113
+ # (A digit, or the point: the value matched, so it's never nil.)
114
+ value = (value * 10) + byte - 48 unless byte == 46 # steep:ignore UnresolvedOverloading
115
+ at += 1
116
+ end
117
+ value
118
+ end
119
+
120
+ # For benchmarks: the worker's server (its socket's path).
121
+ def self.servers = ENV.fetch(SERVERS)
122
+
123
+ # Empties it, over a connection of its own (opened and closed: a Unix
124
+ # socket's is cheap). A flush is an operation, and its bytes count as
125
+ # any client's: so it's for setup or untimed blocks only, and fails
126
+ # while a benchmark is measured (Duel.measuring?).
127
+ def self.flush
128
+ Duel.unmeasured!("Duel::Memcached.flush")
129
+
130
+ connection = Connection.new(servers)
131
+ begin
132
+ connection.flush_all
133
+ ensure
134
+ connection.close
135
+ end
136
+ end
137
+
138
+ # The least a connection needs to read a server's statistics (text
139
+ # protocol): not the code's own, whose operations are what's
140
+ # measured. It counts its own traffic.
141
+ class Connection
142
+ # Its bytes the server had counted when it answered the latest
143
+ # stats: those sent (that stats command's too), and those received
144
+ # before its reply.
145
+ attr_reader :own
146
+
147
+ # The most read at a time: a whole stats reply (some 2.5KB) at once.
148
+ READ = 65_536
149
+
150
+ def self.ready?(path)
151
+ socket = UNIXSocket.new(path)
152
+ begin
153
+ socket.write("version\r\n")
154
+ socket.gets.to_s.start_with?("VERSION ")
155
+ ensure
156
+ socket.close
157
+ end
158
+ rescue SystemCallError, IOError # (JRuby's refusal is an IOError)
159
+ false
160
+ end
161
+
162
+ def initialize(path)
163
+ @socket = UNIXSocket.new(path)
164
+ @sent = @received = @own = 0
165
+ @reply = +""
166
+ @chunk = +""
167
+ end
168
+
169
+ # The server's statistics, by name: each line's name and value
170
+ # sliced from the reply, without a String for each line.
171
+ def stats
172
+ reply = stats_reply
173
+ stats = {} #: Hash[String, String]
174
+ line = 0
175
+ while reply.getbyte(line) == 83 # "S": a STAT line, not the END after them
176
+ name = line + 5 # after "STAT "
177
+ space = reply.byteindex(" ", name) #: Integer
178
+ line = reply.byteindex("\r\n", space) #: Integer
179
+ key = reply.byteslice(name, space - name) #: String
180
+ value = reply.byteslice(space + 1, line - space - 1) #: String
181
+ stats[key] = value
182
+ line += 2
183
+ end
184
+ stats
185
+ end
186
+
187
+ # The server's statistics as it sent them (a line each, then END),
188
+ # read whole into a buffer of the connection's, which the next
189
+ # reading reuses: nothing's allocated.
190
+ def stats_reply
191
+ request("stats\r\n")
192
+ @own = @sent + @received
193
+ @reply.clear
194
+ receive until @reply.end_with?("\r\n") && (!@reply.start_with?("STAT ") || @reply.end_with?("\nEND\r\n"))
195
+ raise "memcached: #{@reply.chomp}" unless @reply.start_with?("STAT ") || @reply == "END\r\n"
196
+
197
+ @reply
198
+ end
199
+
200
+ # Empties the server; returns its reply.
201
+ def flush_all
202
+ request("flush_all\r\n")
203
+ reply
204
+ end
205
+
206
+ def close = @socket.close
207
+
208
+ private
209
+
210
+ # +command+ is a line, its CRLF too. (Written straight to the
211
+ # socket, which write would allocate for.)
212
+ def request(command)
213
+ @sent += @socket.syswrite(command)
214
+ end
215
+
216
+ # Appends what the server sent next to the reply.
217
+ def receive
218
+ @socket.readpartial(READ, @chunk)
219
+ @received += @chunk.bytesize
220
+ @reply << @chunk
221
+ rescue EOFError
222
+ raise IOError, "memcached closed the connection"
223
+ end
224
+
225
+ def reply
226
+ line = @socket.gets or raise IOError, "memcached closed the connection"
227
+ @received += line.bytesize
228
+ line = line.chomp
229
+ raise "memcached: #{line}" if line.match?(/\A(?:ERROR|CLIENT_ERROR|SERVER_ERROR)/)
230
+
231
+ line
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,92 @@
1
+ module Duel
2
+ # A memcached server of each worker's own, and what it did for each
3
+ # benchmark (memcached_operations, memcached_misses, memcached_bytes,
4
+ # and with cpu: true memcached_cpu).
5
+ module Memcached
6
+ VERSION: String
7
+
8
+ SERVERS: String
9
+
10
+ METRICS: Hash[String, Metric::settings]
11
+
12
+ CPU: Metric::settings
13
+
14
+ # The statistics each is the sum of.
15
+ OPERATIONS: Array[String]
16
+ MISSES: Array[String]
17
+ BYTES: Array[String]
18
+ RUSAGE: Array[String]
19
+
20
+ # What a statistic's line starts with, by its name (made once each).
21
+ NEEDLES: Hash[String, String]
22
+
23
+ # Configures Duel for the plugin (c.use :memcached): the server
24
+ # (+server+ is the program, +args+ more of its options, +env+ the
25
+ # variables besides DUEL_MEMCACHED_SERVERS given its socket's path)
26
+ # and its metrics, memcached_cpu too with +cpu+. Returns the plugin's
27
+ # own connection to it, a worker's.
28
+ def self.configure: (Config config, ?server: String, ?args: Array[String], ?env: Array[String], ?cpu: bool) -> InWorker[Connection]
29
+
30
+ # What the worker's server has done so far (but memcached_cpu), read
31
+ # over +connection+ (the plugin's own). It allocates only its Hash.
32
+ def self.stats: (Connection connection) -> Hash[String, Integer]
33
+
34
+ # Its CPU time so far, in microseconds, read over +connection+. It
35
+ # allocates nothing.
36
+ def self.cpu_time: (Connection connection) -> Integer
37
+
38
+ # The sum of +names+ in +stats+, a stats reply as the server sent it.
39
+ def self.sum: (String stats, Array[String] names) -> Integer
40
+
41
+ # The value of +name+ in +stats+ (a stats reply), read in place.
42
+ def self.value: (String stats, String name) -> Integer
43
+
44
+ # For benchmarks: the worker's server (its socket's path).
45
+ def self.servers: () -> String
46
+
47
+ # Empties it, over a connection of its own: in setup or untimed
48
+ # blocks only (it fails while a benchmark is measured).
49
+ def self.flush: () -> String
50
+
51
+ # The least a connection needs to read a server's statistics.
52
+ class Connection
53
+ @socket: UNIXSocket
54
+ @sent: Integer
55
+ @received: Integer
56
+ @reply: String
57
+ @chunk: String
58
+
59
+ # Its bytes the server had counted when it answered the latest stats.
60
+ attr_reader own: Integer
61
+
62
+ # The most read at a time.
63
+ READ: Integer
64
+
65
+ def self.ready?: (String path) -> bool
66
+
67
+ def initialize: (String path) -> void
68
+
69
+ # The server's statistics, by name.
70
+ def stats: () -> Hash[String, String]
71
+
72
+ # The server's statistics as it sent them, in a buffer the next
73
+ # reading reuses.
74
+ def stats_reply: () -> String
75
+
76
+ # Empties the server; returns its reply.
77
+ def flush_all: () -> String
78
+
79
+ def close: () -> void
80
+
81
+ private
82
+
83
+ # +command+ is a line, its CRLF too.
84
+ def request: (String command) -> Integer
85
+
86
+ # Appends what the server sent next to the reply.
87
+ def receive: () -> String
88
+
89
+ def reply: () -> String
90
+ end
91
+ end
92
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: duel-memcached
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Erik Berlin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: duel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ description: 'A Duel plugin: each worker gets a memcached server of its own, and every
27
+ benchmark is judged on the memcached operations, misses and bytes it took too.'
28
+ email:
29
+ - sferik@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - lib/duel/memcached.rb
38
+ - lib/duel/memcached/version.rb
39
+ - sig/duel/memcached.rbs
40
+ homepage: https://github.com/sferik/duel-ruby/tree/main/plugins/memcached
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ source_code_uri: https://github.com/sferik/duel-ruby/tree/main/plugins/memcached
45
+ changelog_uri: https://github.com/sferik/duel-ruby/blob/main/plugins/memcached/CHANGELOG.md
46
+ bug_tracker_uri: https://github.com/sferik/duel-ruby/issues
47
+ rubygems_mfa_required: 'true'
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '3.4'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 4.0.23
63
+ specification_version: 4
64
+ summary: A memcached server for each Duel worker, and what it did for each benchmark
65
+ test_files: []