memcached-server 1.0.1
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 +7 -0
- data/bin/memcached-client +32 -0
- data/bin/memcached-server +14 -0
- data/lib/memcached-server.rb +4 -0
- data/lib/memcached-server/client.rb +210 -0
- data/lib/memcached-server/constants.rb +65 -0
- data/lib/memcached-server/item.rb +101 -0
- data/lib/memcached-server/memcache.rb +195 -0
- data/lib/memcached-server/server.rb +189 -0
- metadata +82 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b24344e4d59a83ed5efc916e5e162c045968e9e54bdac468138d082387cc518a
|
4
|
+
data.tar.gz: fae266a5d5a9cc51c311566fcb6b7632e8f26517cb672213095e443e33695001
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4695cc451824b4ecda4fa3d5b10bf20f3c4d45494f51b49fcdf3a8fe93cacddd4649f84fef49167a1809ac72f5db0debb2474a06bf726692a8fe53ece85d033f
|
7
|
+
data.tar.gz: ba52fb4818a83c87336352157a0cf6c8d02236ab8e11ad818c07a4c78f24f4aeedba3ca30329b1ce0599799de243f53b3669fea461f20c2619f16beda0b08958
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/envy ruby
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require_relative '../lib/memcached-server.rb'
|
5
|
+
|
6
|
+
# Command line arguments
|
7
|
+
hostname = ARGV[0]
|
8
|
+
port = ARGV[1]
|
9
|
+
|
10
|
+
socket = TCPSocket.new(hostname, port)
|
11
|
+
|
12
|
+
# Listener thread
|
13
|
+
listener = Thread.new {
|
14
|
+
while line = socket.gets()
|
15
|
+
puts(line)
|
16
|
+
end
|
17
|
+
}
|
18
|
+
|
19
|
+
# Speaker thread
|
20
|
+
speaker = Thread.new {
|
21
|
+
loop do
|
22
|
+
command = STDIN.gets()
|
23
|
+
socket.puts(command)
|
24
|
+
break if $_.match(MemcachedServer::Reply::END_) # $_ : the last input line of string by gets or readline. Thread and scope local.
|
25
|
+
sleep(0.1)
|
26
|
+
end
|
27
|
+
}
|
28
|
+
|
29
|
+
listener.join()
|
30
|
+
speaker.join()
|
31
|
+
|
32
|
+
socket.close()
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/envy ruby
|
2
|
+
|
3
|
+
require_relative '../lib/memcached-server.rb'
|
4
|
+
|
5
|
+
# Command line arguments
|
6
|
+
hostname = ARGV[0]
|
7
|
+
port = ARGV[1]
|
8
|
+
|
9
|
+
server = MemcachedServer::Server.new(hostname, port)
|
10
|
+
puts ('Server running on port %d' % server.port)
|
11
|
+
|
12
|
+
run = Thread.new {server.run()}
|
13
|
+
|
14
|
+
run.join()
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative 'constants.rb'
|
3
|
+
|
4
|
+
module MemcachedServer
|
5
|
+
|
6
|
+
# Class that communicates with a MemcachedServer::Server
|
7
|
+
class Client
|
8
|
+
|
9
|
+
# The client socket hostname or IP address
|
10
|
+
#
|
11
|
+
# @return [String, ipaddress]
|
12
|
+
attr_reader :hostname
|
13
|
+
|
14
|
+
# The client socket port
|
15
|
+
#
|
16
|
+
# @return [port]
|
17
|
+
attr_reader :port
|
18
|
+
|
19
|
+
def initialize(hostname, port)
|
20
|
+
|
21
|
+
@hostname = hostname
|
22
|
+
@port = port
|
23
|
+
@server = TCPSocket.new(hostname, port)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sends the server a set command
|
28
|
+
#
|
29
|
+
# @param key [String] The key of the item to store
|
30
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
31
|
+
# @param exptime [Integer] The exptime of the Item to store
|
32
|
+
# @param bytes [Integer] The byte size of <data_block>
|
33
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
34
|
+
# @return [String] The reply that describes the result of the operation
|
35
|
+
def set(key, flags, exptime, bytes, data_block)
|
36
|
+
command = "set #{key} #{flags} #{exptime} #{bytes}\n#{data_block}\n"
|
37
|
+
@server.puts(command)
|
38
|
+
|
39
|
+
return @server.gets()
|
40
|
+
end
|
41
|
+
|
42
|
+
# Sends the server an add command
|
43
|
+
#
|
44
|
+
# @param key [String] The key of the item to store
|
45
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
46
|
+
# @param exptime [Integer] The exptime of the Item to store
|
47
|
+
# @param bytes [Integer] The byte size of <data_block>
|
48
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
49
|
+
# @return [String] The reply that describes the result of the operation
|
50
|
+
def add(key, flags, exptime, bytes, data_block)
|
51
|
+
command = "add #{key} #{flags} #{exptime} #{bytes}\n#{data_block}\n"
|
52
|
+
@server.puts(command)
|
53
|
+
|
54
|
+
return @server.gets()
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# Sends the server a replace command
|
59
|
+
#
|
60
|
+
# @param key [String] The key of the item to store
|
61
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
62
|
+
# @param exptime [Integer] The exptime of the Item to store
|
63
|
+
# @param bytes [Integer] The byte size of <data_block>
|
64
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
65
|
+
# @return [String] The reply that describes the result of the operation
|
66
|
+
def replace(key, flags, exptime, bytes, data_block)
|
67
|
+
command = "replace #{key} #{flags} #{exptime} #{bytes}\n#{data_block}\n"
|
68
|
+
@server.puts(command)
|
69
|
+
|
70
|
+
return @server.gets()
|
71
|
+
end
|
72
|
+
|
73
|
+
# Sends the server an append command
|
74
|
+
#
|
75
|
+
# @param key [String] The key of the item to store
|
76
|
+
# @param bytes [Integer] The byte size of <data_block>
|
77
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
78
|
+
# @return [String] The reply that describes the result of the operation
|
79
|
+
def append(key, bytes, data_block)
|
80
|
+
command = "append #{key} #{bytes}\n#{data_block}\n"
|
81
|
+
@server.puts(command)
|
82
|
+
|
83
|
+
return @server.gets()
|
84
|
+
end
|
85
|
+
|
86
|
+
# Sends the server a prepend command
|
87
|
+
#
|
88
|
+
# @param key [String] The key of the item to store
|
89
|
+
# @param bytes [Integer] The byte size of <data_block>
|
90
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
91
|
+
# @return [String] The reply that describes the result of the operation
|
92
|
+
def prepend(key, bytes, data_block)
|
93
|
+
command = "prepend #{key} #{bytes}\n#{data_block}\n"
|
94
|
+
@server.puts(command)
|
95
|
+
|
96
|
+
return @server.gets()
|
97
|
+
end
|
98
|
+
|
99
|
+
# Sends the server a cas command
|
100
|
+
#
|
101
|
+
# @param key [String] The key of the item to store
|
102
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
103
|
+
# @param exptime [Integer] The exptime of the Item to store
|
104
|
+
# @param bytes [Integer] The byte size of <data_block>
|
105
|
+
# @param cas_id [Integer] Is a unique integer value
|
106
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
107
|
+
# @return [String] The reply that describes the result of the operation
|
108
|
+
def cas(key, flags, exptime, bytes, cas_id, data_block)
|
109
|
+
command = "cas #{key} #{flags} #{exptime} #{bytes} #{cas_id}\n#{data_block}\n"
|
110
|
+
@server.puts(command)
|
111
|
+
|
112
|
+
return @server.gets()
|
113
|
+
end
|
114
|
+
|
115
|
+
# Sends the server a get command
|
116
|
+
#
|
117
|
+
# @param keys [[String]] The keys of the items to retrieve
|
118
|
+
# @return [[MemcachedServer::Item]] Array of the retrieved MemcachedServer::Item instances
|
119
|
+
def get(keys)
|
120
|
+
@server.puts("get #{keys}")
|
121
|
+
|
122
|
+
n = keys.split(' ').length()
|
123
|
+
retrieved = {}
|
124
|
+
|
125
|
+
n.times do
|
126
|
+
loop do
|
127
|
+
|
128
|
+
case @server.gets()
|
129
|
+
|
130
|
+
when ReplyFormat::GET
|
131
|
+
|
132
|
+
key = $~[:key]
|
133
|
+
flags = $~[:flags].to_i()
|
134
|
+
bytes = $~[:bytes].to_i()
|
135
|
+
data_block = @server.read(bytes + 1).chomp()
|
136
|
+
|
137
|
+
item = Item.new(key, flags, 0, bytes, data_block)
|
138
|
+
retrieved[key.to_sym] = item
|
139
|
+
|
140
|
+
when ReplyFormat::END_
|
141
|
+
|
142
|
+
break
|
143
|
+
|
144
|
+
else
|
145
|
+
|
146
|
+
puts "Error\nServer: #{$_}"
|
147
|
+
break
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
return retrieved
|
156
|
+
end
|
157
|
+
|
158
|
+
# Sends the server a gets command
|
159
|
+
#
|
160
|
+
# @param keys [[String]] The keys of the items to retrieve
|
161
|
+
# @return [[MemcachedServer::Item]] Array of the retrieved MemcachedServer::Item instances
|
162
|
+
def gets(keys)
|
163
|
+
@server.puts("gets #{keys}")
|
164
|
+
|
165
|
+
n = keys.split(' ').length()
|
166
|
+
retrieved = {}
|
167
|
+
|
168
|
+
n.times do
|
169
|
+
|
170
|
+
loop do
|
171
|
+
|
172
|
+
case @server.gets()
|
173
|
+
when ReplyFormat::GETS
|
174
|
+
key = $~[:key]
|
175
|
+
flags = $~[:flags].to_i()
|
176
|
+
bytes = $~[:bytes].to_i()
|
177
|
+
cas_id = $~[:cas_id].to_i()
|
178
|
+
data_block = @server.read(bytes + 1).chomp()
|
179
|
+
|
180
|
+
item = Item.new(key, flags, 0, bytes, data_block)
|
181
|
+
item.cas_id = cas_id
|
182
|
+
retrieved[key.to_sym] = item
|
183
|
+
|
184
|
+
when ReplyFormat::END_
|
185
|
+
break
|
186
|
+
|
187
|
+
else
|
188
|
+
puts "Error\nServer: #{$_}"
|
189
|
+
break
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
return retrieved
|
198
|
+
end
|
199
|
+
|
200
|
+
# Sends the server an end command
|
201
|
+
#
|
202
|
+
# @return [String] The reply that describes the result of the operation
|
203
|
+
def end()
|
204
|
+
@server.puts('END')
|
205
|
+
return @server.gets()
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module MemcachedServer
|
2
|
+
|
3
|
+
module CommandFormat
|
4
|
+
# \w - A word character ([a-zA-Z0-9_])
|
5
|
+
# \d - A digit character ([0-9])
|
6
|
+
|
7
|
+
# Storage commands format
|
8
|
+
SET = /^(?<name>set) (?<key>(\w)+) (?<flags>\d+) (?<exptime>\d+) (?<bytes>\d+)(?<noreply>noreply)?\n/.freeze
|
9
|
+
ADD = /^(?<name>add) (?<key>(\w)+) (?<flags>\d+) (?<exptime>\d+) (?<bytes>\d+)(?<noreply>noreply)?\n/.freeze
|
10
|
+
REPLACE = /^(?<name>replace) (?<key>(\w)+) (?<flags>\d+) (?<exptime>\d+) (?<bytes>\d+)(?<noreply>noreply)?\n/.freeze
|
11
|
+
APPEND = /^(?<name>append) (?<key>(\w)+) (?<bytes>\d+)(?<noreply>noreply)?\n/.freeze
|
12
|
+
PREPEND = /^(?<name>prepend) (?<key>(\w)+) (?<bytes>\d+)(?<noreply>noreply)?\n/.freeze
|
13
|
+
CAS = /^(?<name>cas) (?<key>(\w)+) (?<flags>\d+) (?<exptime>\d+) (?<bytes>\d+) (?<cas_id>\d+)(?<noreply>noreply)?\n/.freeze
|
14
|
+
|
15
|
+
# Retrieval commands format
|
16
|
+
GET = /^(?<name>get) (?<keys>(\w|\p{Space})+)\n/.freeze
|
17
|
+
GETS = /^(?<name>gets) (?<keys>(\w|\p{Space})+)\n/.freeze
|
18
|
+
|
19
|
+
# End command format
|
20
|
+
END_ = /^(?<name>END)\n$/.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
module Reply
|
24
|
+
|
25
|
+
# To indicate success.
|
26
|
+
STORED = "STORED\n".freeze
|
27
|
+
|
28
|
+
# To indicate the data was not stored, but not because of an error.
|
29
|
+
# This normally means that the condition for an "add" or a "replace" command wasn't met.
|
30
|
+
NOT_STORED = "NOT_STORED\n".freeze
|
31
|
+
|
32
|
+
# To indicate that the item you are trying to store with a "cas" command has been modified since you last fetched it.
|
33
|
+
EXISTS = "EXISTS\n".freeze
|
34
|
+
|
35
|
+
# To indicate that the item you are trying to store with a "cas" command did not exist.
|
36
|
+
NOT_FOUND = "NOT_FOUND\n".freeze
|
37
|
+
|
38
|
+
# Each item sent by the server looks like this
|
39
|
+
GET = "VALUE %s %d %d\n%s\n".freeze
|
40
|
+
GETS = "VALUE %s %d %d %d\n%s\n".freeze
|
41
|
+
|
42
|
+
END_ = "END\n".freeze
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
module ReplyFormat
|
47
|
+
|
48
|
+
# Each item sent by the server has this format
|
49
|
+
GET = /VALUE (?<key>\w+) (?<flags>\d+) (?<bytes>\d+)/.freeze
|
50
|
+
GETS = /VALUE (?<key>\w+) (?<flags>\d+) (?<bytes>\d+) (?<cas_id>\d+)/.freeze
|
51
|
+
|
52
|
+
# To indicate the end of reply.
|
53
|
+
END_ = /END/.freeze
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
module Error
|
58
|
+
|
59
|
+
ERROR = "ERROR\r\n".freeze
|
60
|
+
CLIENT_ERROR = "CLIENT_ERROR%s\r\n".freeze
|
61
|
+
SERVER_ERROR = "SERVER_ERROR%s\r\n".freeze
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module MemcachedServer
|
2
|
+
|
3
|
+
# Class that wraps up a Memcached Item
|
4
|
+
class Item
|
5
|
+
|
6
|
+
# The key under which the client asks to store the data
|
7
|
+
#
|
8
|
+
# @return [String]
|
9
|
+
attr_accessor :key
|
10
|
+
|
11
|
+
# Is an arbitrary unsigned integer (written out in decimal)
|
12
|
+
#
|
13
|
+
# @return [Integer]
|
14
|
+
attr_accessor :flags
|
15
|
+
|
16
|
+
# The expiration time
|
17
|
+
#
|
18
|
+
# @return [Integer]
|
19
|
+
attr_accessor :exptime
|
20
|
+
|
21
|
+
# The bite size of <data_block>
|
22
|
+
#
|
23
|
+
# @return [Integer]
|
24
|
+
attr_accessor :bytes
|
25
|
+
|
26
|
+
# Is a chunk of arbitrary 8-bit data of length <bytes>
|
27
|
+
#
|
28
|
+
# @return [Hash]
|
29
|
+
attr_accessor :data_block
|
30
|
+
|
31
|
+
# A unique integer value
|
32
|
+
#
|
33
|
+
# @return [Integer]
|
34
|
+
attr_accessor :cas_id
|
35
|
+
|
36
|
+
# A simple semaphore that can be used to coordinate access to shared data from multiple concurrent threads.
|
37
|
+
#
|
38
|
+
# @return [Mutex]
|
39
|
+
attr_accessor :lock
|
40
|
+
|
41
|
+
@@last_cas_id = 0
|
42
|
+
|
43
|
+
def initialize(key, flags, exptime, bytes, data_block)
|
44
|
+
|
45
|
+
@key = key
|
46
|
+
@flags = flags
|
47
|
+
@exptime = get_exptime(exptime)
|
48
|
+
@bytes = bytes
|
49
|
+
@data_block = data_block
|
50
|
+
|
51
|
+
@lock = Mutex.new()
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
# Gets the next cas_id value read from #last_cas_id class variable
|
56
|
+
#
|
57
|
+
# @return [Integer] The next cas_id
|
58
|
+
def get_cas_id()
|
59
|
+
|
60
|
+
@lock.synchronize do
|
61
|
+
|
62
|
+
@@last_cas_id += 1
|
63
|
+
next_id = @@last_cas_id.dup()
|
64
|
+
|
65
|
+
return next_id
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Updates the MemcachedServer::Item #cas_id with the corresponding next value read from #last_cas_id class variable
|
71
|
+
def update_cas_id()
|
72
|
+
|
73
|
+
@cas_id = get_cas_id()
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# Parses the exptime of the MemcachedServer::Item instance
|
78
|
+
#
|
79
|
+
# @param exptime [Integer] The expiration time
|
80
|
+
# @return [Time, nil] The expiration time
|
81
|
+
def get_exptime(exptime)
|
82
|
+
|
83
|
+
return nil if exptime == 0
|
84
|
+
return Time.now().getutc() if exptime < 0
|
85
|
+
return Time.now().getutc() + exptime
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
# Checks if a MemcachedServer::Item instance is expired
|
90
|
+
#
|
91
|
+
# @return [Boolean] true if it's expired and otherwise false
|
92
|
+
def expired?()
|
93
|
+
|
94
|
+
return true if (!@exptime.nil?()) && (Time.now().getutc() > @exptime)
|
95
|
+
return false
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require_relative './item.rb'
|
2
|
+
require_relative './constants.rb'
|
3
|
+
|
4
|
+
module MemcachedServer
|
5
|
+
|
6
|
+
# The class used to process Memcache commands and store the Memcache server data
|
7
|
+
class Memcache
|
8
|
+
|
9
|
+
# The Hash map used to store the Memcache server data
|
10
|
+
#
|
11
|
+
# @return [Hash]
|
12
|
+
attr_reader :storage
|
13
|
+
|
14
|
+
def initialize()
|
15
|
+
|
16
|
+
@storage = Hash.new()
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
# Deletes @storage items if they are expired
|
21
|
+
def purge_keys()
|
22
|
+
|
23
|
+
@storage.delete_if { | key, item | item.expired? }
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieves the items corresponding to the given keys from @storage
|
28
|
+
#
|
29
|
+
# @param keys [[String]] Array that contains the keys of the items to retrieve
|
30
|
+
# @return [[MemcachedServer::Item]] Array of the retrieved MemcachedServer::Item instances
|
31
|
+
def get(keys)
|
32
|
+
|
33
|
+
purge_keys()
|
34
|
+
|
35
|
+
items = @storage.values_at(*keys)
|
36
|
+
return items
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# Stores a MemcachedServer::Item, with the attributes recieved by param, in @storage
|
41
|
+
# Depends on #store_item method
|
42
|
+
#
|
43
|
+
# @param key [String] The key of the item to store
|
44
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
45
|
+
# @param exptime [Integer] The exptime of the Item to store
|
46
|
+
# @param bytes [Integer] The byte size of <data_block>
|
47
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
48
|
+
# @return [String] The reply that describes the result of the operation
|
49
|
+
def set(key, flags, exptime, bytes, data_block)
|
50
|
+
|
51
|
+
store_item(key, flags, exptime, bytes, data_block)
|
52
|
+
return Reply::STORED
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# Stores a MemcachedServer::Item, with the attributes recieved by param, in @storage only if it isn't already stored
|
57
|
+
# Depends on #store_item method
|
58
|
+
#
|
59
|
+
# @param key [String] The key of the item to store
|
60
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
61
|
+
# @param exptime [Integer] The exptime of the Item to store
|
62
|
+
# @param bytes [Integer] The byte size of <data_block>
|
63
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
64
|
+
# @return [String] The reply that describes the result of the operation
|
65
|
+
def add(key, flags, exptime, bytes, data_block)
|
66
|
+
|
67
|
+
purge_keys()
|
68
|
+
|
69
|
+
return Reply::NOT_STORED if @storage.key?(key)
|
70
|
+
|
71
|
+
store_item(key, flags, exptime, bytes, data_block)
|
72
|
+
return Reply::STORED
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
# Replaces a MemcachedServer::Item stored in @storage, with a new one with the attributes recieved by param
|
77
|
+
# Depends on #store_item method
|
78
|
+
#
|
79
|
+
# @param key [String] The key of the item to store
|
80
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
81
|
+
# @param exptime [Integer] The exptime of the Item to store
|
82
|
+
# @param bytes [Integer] The byte size of <data_block>
|
83
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
84
|
+
# @return [String] The reply that describes the result of the operation
|
85
|
+
def replace(key, flags, exptime, bytes, data_block)
|
86
|
+
|
87
|
+
return Reply::NOT_STORED unless @storage.key?(key)
|
88
|
+
|
89
|
+
store_item(key, flags, exptime, bytes, data_block)
|
90
|
+
return Reply::STORED
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
# Appends <new_data> to a MemcachedServer::Item data_block that is stored in @storage with an associated <key>
|
95
|
+
#
|
96
|
+
# @param key [String] The key of the item to store
|
97
|
+
# @param bytes [Integer] The byte size of <new_data>
|
98
|
+
# @param new_data [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
99
|
+
# @return [String] The reply that describes the result of the operation
|
100
|
+
def append(key, bytes, new_data)
|
101
|
+
|
102
|
+
purge_keys()
|
103
|
+
|
104
|
+
return Reply::NOT_STORED unless @storage.key?(key)
|
105
|
+
|
106
|
+
item = @storage[key]
|
107
|
+
item.lock.synchronize do
|
108
|
+
|
109
|
+
item.data_block.concat(new_data)
|
110
|
+
item.bytes += bytes
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
item.update_cas_id()
|
115
|
+
return Reply::STORED
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
# Prepends <new_data> to a MemcachedServer::Item data_block that is stored in @storage with an associated <key>
|
120
|
+
#
|
121
|
+
# @param key [String] The key of the item to store
|
122
|
+
# @param bytes [Integer] The byte size of <new_data>
|
123
|
+
# @param new_data [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
124
|
+
# @return [String] The reply that describes the result of the operation
|
125
|
+
def prepend(key, bytes, new_data)
|
126
|
+
|
127
|
+
purge_keys()
|
128
|
+
|
129
|
+
return Reply::NOT_STORED unless @storage.key?(key)
|
130
|
+
|
131
|
+
item = @storage[key]
|
132
|
+
item.lock.synchronize do
|
133
|
+
|
134
|
+
item.data_block.prepend(new_data)
|
135
|
+
item.bytes += bytes
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
item.update_cas_id()
|
140
|
+
return Reply::STORED
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
# Check and set operation that stores a MemcachedServer::Item only if no one else has updated since it was last fetched
|
145
|
+
# Depends on #store_item method
|
146
|
+
#
|
147
|
+
# @param key [String] The key of the item to store
|
148
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
149
|
+
# @param exptime [Integer] The exptime of the Item to store
|
150
|
+
# @param bytes [Integer] The byte size of <data_block>
|
151
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
152
|
+
# @param cas_id [Integer] Is a unique integer value
|
153
|
+
# @return [String] The reply that describes the result of the operation
|
154
|
+
def cas(key, flags, exptime, bytes, cas_id, data_block)
|
155
|
+
|
156
|
+
purge_keys()
|
157
|
+
|
158
|
+
return Reply::NOT_FOUND unless @storage.key?(key)
|
159
|
+
|
160
|
+
item = @storage[key]
|
161
|
+
item.lock.synchronize do
|
162
|
+
|
163
|
+
return Reply::EXISTS if cas_id != item.cas_id
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
store_item(key, flags, exptime, bytes, data_block)
|
168
|
+
return Reply::STORED
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
# Stores a MemcachedServer::Item, with the attributes recieved by param, in @storage
|
173
|
+
# Before storing the MemcachedServer::Item it updates it's cas_id
|
174
|
+
#
|
175
|
+
# @param key [String] The key of the item to store
|
176
|
+
# @param flags [Integer] Is an arbitrary unsigned integer (written out in decimal)
|
177
|
+
# @param exptime [Integer] The exptime of the Item to store
|
178
|
+
# @param bytes [Integer] The byte size of <data_block>
|
179
|
+
# @param data_block [String] Is a chunk of arbitrary 8-bit data of length <bytes>
|
180
|
+
# @return [String] The reply that describes the result of the operation
|
181
|
+
def store_item(key, flags, exptime, bytes, data_block)
|
182
|
+
|
183
|
+
item = Item.new(key, flags, exptime, bytes, data_block)
|
184
|
+
item.update_cas_id()
|
185
|
+
item.lock.synchronize do
|
186
|
+
|
187
|
+
@storage.store(key, item) unless item.expired?()
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative './memcache.rb'
|
3
|
+
require_relative './constants.rb'
|
4
|
+
|
5
|
+
module MemcachedServer
|
6
|
+
|
7
|
+
# Class that wraps up a Memcached server
|
8
|
+
class Server
|
9
|
+
|
10
|
+
# The server hostname or IP address
|
11
|
+
#
|
12
|
+
# @return [String, ipaddress]
|
13
|
+
attr_reader :hostname
|
14
|
+
|
15
|
+
# The server port
|
16
|
+
#
|
17
|
+
# @return [port]
|
18
|
+
attr_reader :port
|
19
|
+
|
20
|
+
# The Memcache object that implements the logic of the Memcache protocol
|
21
|
+
#
|
22
|
+
# @return [MemcachedServer::Memcache]
|
23
|
+
attr_reader :mc
|
24
|
+
|
25
|
+
def initialize(hostname, port)
|
26
|
+
|
27
|
+
@hostname = hostname
|
28
|
+
@port = port
|
29
|
+
@connection = TCPServer.new(hostname, port)
|
30
|
+
@mc = Memcache.new()
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# Starts the server
|
35
|
+
def run()
|
36
|
+
|
37
|
+
begin
|
38
|
+
loop do
|
39
|
+
Thread.start(@connection.accept()) do | connection |
|
40
|
+
|
41
|
+
puts("New connection: #{connection.to_s}.")
|
42
|
+
|
43
|
+
close = false
|
44
|
+
while command = connection.gets()
|
45
|
+
|
46
|
+
puts("Command: #{command} | Connection: #{connection.to_s}")
|
47
|
+
|
48
|
+
valid_command = validate_command(command)
|
49
|
+
if valid_command
|
50
|
+
close = run_command(connection, valid_command)
|
51
|
+
else
|
52
|
+
connection.puts(Error::CLIENT_ERROR % [" Undefined command. Please check the command syntax and try again."])
|
53
|
+
end
|
54
|
+
|
55
|
+
break if close
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
connection.puts(Reply::END_)
|
60
|
+
connection.close()
|
61
|
+
puts ("Connection closed to: #{connection}.")
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
rescue => exception
|
67
|
+
error = Error::SERVER_ERROR % exception.message
|
68
|
+
connection.puts(error)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Runs a valid memcache command
|
73
|
+
#
|
74
|
+
# Depends on MemcachedServer::Memcache method names.
|
75
|
+
# In some cases, to make #send method work the MemcachedServer::Memcache
|
76
|
+
# corresponding method must be equal to valid_command[:name]
|
77
|
+
#
|
78
|
+
# @param connection [TCPSocket] Client's socket
|
79
|
+
# @param valid_command [MatchData] It encapsulates all the results of a valid command pattern match
|
80
|
+
# @return [Boolean] false if valid_command[:name] != END else true
|
81
|
+
def run_command(connection, valid_command)
|
82
|
+
|
83
|
+
name = valid_command[:name]
|
84
|
+
|
85
|
+
case name
|
86
|
+
when 'set', 'add', 'replace'
|
87
|
+
|
88
|
+
key = valid_command[:key]
|
89
|
+
flags = valid_command[:flags].to_i
|
90
|
+
exptime = valid_command[:exptime].to_i
|
91
|
+
bytes = valid_command[:bytes].to_i
|
92
|
+
noreply = !valid_command[:noreply].nil?
|
93
|
+
data = self.read_bytes(connection, bytes)
|
94
|
+
|
95
|
+
reply = @mc.send(name.to_sym, key, flags, exptime, bytes, data)
|
96
|
+
connection.puts(reply) unless noreply
|
97
|
+
|
98
|
+
return false
|
99
|
+
|
100
|
+
when 'append', 'prepend'
|
101
|
+
|
102
|
+
key = valid_command[:key]
|
103
|
+
bytes = valid_command[:bytes].to_i
|
104
|
+
data = self.read_bytes(connection, bytes)
|
105
|
+
|
106
|
+
reply = @mc.send(name.to_sym, key, bytes, data)
|
107
|
+
connection.puts(reply) unless noreply
|
108
|
+
|
109
|
+
return false
|
110
|
+
|
111
|
+
when 'cas'
|
112
|
+
|
113
|
+
key = valid_command[:key]
|
114
|
+
flags = valid_command[:flags].to_i
|
115
|
+
exptime = valid_command[:exptime].to_i
|
116
|
+
bytes = valid_command[:bytes].to_i
|
117
|
+
noreply = !valid_command[:noreply].nil?
|
118
|
+
data = self.read_bytes(connection, bytes)
|
119
|
+
cas_id = valid_command[:cas_id].to_i()
|
120
|
+
|
121
|
+
reply = @mc.cas(key, flags, exptime, bytes, cas_id, data)
|
122
|
+
connection.puts(reply) unless noreply
|
123
|
+
|
124
|
+
return false
|
125
|
+
|
126
|
+
when 'get'
|
127
|
+
|
128
|
+
keys = valid_command[:keys].split(' ')
|
129
|
+
items = @mc.get(keys)
|
130
|
+
|
131
|
+
for item in items
|
132
|
+
connection.puts(Reply::GET % [item.key, item.flags, item.bytes, item.data_block]) if item
|
133
|
+
connection.puts(Reply::END_)
|
134
|
+
end
|
135
|
+
|
136
|
+
return false
|
137
|
+
|
138
|
+
when 'gets'
|
139
|
+
|
140
|
+
keys = valid_command[:keys].split(' ')
|
141
|
+
items = @mc.get(keys)
|
142
|
+
|
143
|
+
for item in items
|
144
|
+
connection.puts(Reply::GETS % [item.key, item.flags, item.bytes, item.cas_id, item.data_block]) if item
|
145
|
+
connection.puts(Reply::END_)
|
146
|
+
end
|
147
|
+
|
148
|
+
return false
|
149
|
+
|
150
|
+
else
|
151
|
+
# END command stops run
|
152
|
+
return true
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Reads <bytes> bytes from <connection>
|
158
|
+
#
|
159
|
+
# @param connection [TCPSocket] Client's socket
|
160
|
+
# @param bytes [Integer] The number of bytes to read
|
161
|
+
# @return [String] The message read
|
162
|
+
def read_bytes(connection, bytes)
|
163
|
+
|
164
|
+
return connection.read(bytes + 1).chomp()
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
# Validates a command.
|
169
|
+
# If the command isn't valid it returns nil.
|
170
|
+
#
|
171
|
+
# @param command [String] A command to validate
|
172
|
+
# @return [MatchData, nil] It encapsulates all the results of a valid command pattern match
|
173
|
+
def validate_command(command)
|
174
|
+
|
175
|
+
valid_formats = CommandFormat.constants.map{| key | CommandFormat.const_get(key)}
|
176
|
+
|
177
|
+
valid_formats.each do | form |
|
178
|
+
|
179
|
+
valid_command = command.match(form)
|
180
|
+
return valid_command unless valid_command.nil?
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
return nil
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: memcached-server
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- José Andrés Puglia Laca
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.10.1
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.10.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 13.0.1
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 13.0.1
|
41
|
+
description: A simple Memcached server (TCP/IP socket) that complies with the specified
|
42
|
+
protocol. Implemented in Ruby.
|
43
|
+
email:
|
44
|
+
- andrespuglia98@gmail.com
|
45
|
+
executables:
|
46
|
+
- memcached-server
|
47
|
+
- memcached-client
|
48
|
+
extensions: []
|
49
|
+
extra_rdoc_files: []
|
50
|
+
files:
|
51
|
+
- bin/memcached-client
|
52
|
+
- bin/memcached-server
|
53
|
+
- lib/memcached-server.rb
|
54
|
+
- lib/memcached-server/client.rb
|
55
|
+
- lib/memcached-server/constants.rb
|
56
|
+
- lib/memcached-server/item.rb
|
57
|
+
- lib/memcached-server/memcache.rb
|
58
|
+
- lib/memcached-server/server.rb
|
59
|
+
homepage: https://github.com/AndresPuglia98/memcached-server
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubygems_version: 3.0.8
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: A simple Memcached server implemented in Ruby.
|
82
|
+
test_files: []
|