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 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,4 @@
1
+ require_relative './memcached-server/memcache.rb'
2
+ require_relative './memcached-server/server.rb'
3
+ require_relative './memcached-server/client.rb'
4
+ require_relative './memcached-server/constants.rb'
@@ -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: []