jashmenn-dalli 1.0.3
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.
- data/Gemfile +7 -0
- data/History.md +169 -0
- data/LICENSE +20 -0
- data/Performance.md +77 -0
- data/README.md +177 -0
- data/Rakefile +34 -0
- data/Upgrade.md +45 -0
- data/dalli.gemspec +33 -0
- data/lib/action_controller/session/dalli_store.rb +62 -0
- data/lib/action_dispatch/middleware/session/dalli_store.rb +67 -0
- data/lib/active_support/cache/dalli_store.rb +185 -0
- data/lib/active_support/cache/dalli_store23.rb +172 -0
- data/lib/dalli.rb +43 -0
- data/lib/dalli/client.rb +264 -0
- data/lib/dalli/compatibility.rb +52 -0
- data/lib/dalli/memcache-client.rb +1 -0
- data/lib/dalli/options.rb +44 -0
- data/lib/dalli/ring.rb +105 -0
- data/lib/dalli/server.rb +516 -0
- data/lib/dalli/socket.rb +37 -0
- data/lib/dalli/version.rb +3 -0
- data/test/abstract_unit.rb +284 -0
- data/test/benchmark_test.rb +170 -0
- data/test/helper.rb +54 -0
- data/test/memcached_mock.rb +106 -0
- data/test/test_active_support.rb +177 -0
- data/test/test_compatibility.rb +33 -0
- data/test/test_dalli.rb +398 -0
- data/test/test_encoding.rb +34 -0
- data/test/test_failover.rb +89 -0
- data/test/test_network.rb +54 -0
- data/test/test_ring.rb +89 -0
- data/test/test_sasl.rb +79 -0
- data/test/test_session_store.rb +230 -0
- metadata +145 -0
data/lib/dalli.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'dalli/client'
|
2
|
+
require 'dalli/ring'
|
3
|
+
require 'dalli/server'
|
4
|
+
require 'dalli/socket'
|
5
|
+
require 'dalli/version'
|
6
|
+
require 'dalli/options'
|
7
|
+
|
8
|
+
unless ''.respond_to?(:bytesize)
|
9
|
+
class String
|
10
|
+
alias_method :bytesize, :size
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Dalli
|
15
|
+
# generic error
|
16
|
+
class DalliError < RuntimeError; end
|
17
|
+
# socket/server communication error
|
18
|
+
class NetworkError < DalliError; end
|
19
|
+
# no server available/alive error
|
20
|
+
class RingError < DalliError; end
|
21
|
+
# application error in marshalling
|
22
|
+
class MarshalError < DalliError; end
|
23
|
+
|
24
|
+
def self.logger
|
25
|
+
@logger ||= (rails_logger || default_logger)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.rails_logger
|
29
|
+
(defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
|
30
|
+
(defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:debug) && RAILS_DEFAULT_LOGGER)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.default_logger
|
34
|
+
require 'logger'
|
35
|
+
l = Logger.new(STDOUT)
|
36
|
+
l.level = Logger::INFO
|
37
|
+
l
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.logger=(logger)
|
41
|
+
@logger = logger
|
42
|
+
end
|
43
|
+
end
|
data/lib/dalli/client.rb
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
# encoding: ascii
|
2
|
+
module Dalli
|
3
|
+
class Client
|
4
|
+
|
5
|
+
##
|
6
|
+
# Dalli::Client is the main class which developers will use to interact with
|
7
|
+
# the memcached server. Usage:
|
8
|
+
#
|
9
|
+
# Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5'],
|
10
|
+
# :threadsafe => true, :failover => true, :expires_in => 300)
|
11
|
+
#
|
12
|
+
# servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly.
|
13
|
+
# Both weight and port are optional. If you pass in nil, Dalli will default to 'localhost:11211'.
|
14
|
+
# Note that the <tt>MEMCACHE_SERVERS</tt> environment variable will override the servers parameter for use
|
15
|
+
# in managed environments like Heroku.
|
16
|
+
#
|
17
|
+
# Options:
|
18
|
+
# - :failover - if a server is down, look for and store values on another server in the ring. Default: true.
|
19
|
+
# - :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
|
20
|
+
# - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults to 0 or forever
|
21
|
+
# - :compression - defaults to false, if true Dalli will compress values larger than 100 bytes before
|
22
|
+
# sending them to memcached.
|
23
|
+
#
|
24
|
+
def initialize(servers=nil, options={})
|
25
|
+
@servers = env_servers || servers || 'localhost:11211'
|
26
|
+
@options = { :expires_in => 0 }.merge(options)
|
27
|
+
self.extend(Dalli::Client::MemcacheClientCompatibility) if Dalli::Client.compatibility_mode
|
28
|
+
@ring = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Turn on compatibility mode, which mixes in methods in memcache_client_compatibility.rb
|
33
|
+
# This value is set to true in memcache-client.rb.
|
34
|
+
def self.compatibility_mode
|
35
|
+
@compatibility_mode ||= false
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.compatibility_mode=(compatibility_mode)
|
39
|
+
require 'dalli/compatibility'
|
40
|
+
@compatibility_mode = compatibility_mode
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# The standard memcached instruction set
|
45
|
+
#
|
46
|
+
|
47
|
+
##
|
48
|
+
# Turn on quiet aka noreply support.
|
49
|
+
# All relevant operations within this block with be effectively
|
50
|
+
# pipelined as Dalli will use 'quiet' operations where possible.
|
51
|
+
# Currently supports the set, add, replace and delete operations.
|
52
|
+
def multi
|
53
|
+
old, Thread.current[:dalli_multi] = Thread.current[:dalli_multi], true
|
54
|
+
yield
|
55
|
+
ensure
|
56
|
+
Thread.current[:dalli_multi] = old
|
57
|
+
end
|
58
|
+
|
59
|
+
def get(key, options=nil)
|
60
|
+
resp = perform(:get, key)
|
61
|
+
(!resp || resp == 'Not found') ? nil : resp
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Fetch multiple keys efficiently.
|
66
|
+
# Returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
67
|
+
def get_multi(*keys)
|
68
|
+
return {} if keys.empty?
|
69
|
+
options = nil
|
70
|
+
options = keys.pop if keys.last.is_a?(Hash) || keys.last.nil?
|
71
|
+
ring.lock do
|
72
|
+
keys.flatten.each do |key|
|
73
|
+
perform(:getkq, key)
|
74
|
+
end
|
75
|
+
|
76
|
+
values = {}
|
77
|
+
ring.servers.each do |server|
|
78
|
+
next unless server.alive?
|
79
|
+
begin
|
80
|
+
server.request(:noop).each_pair do |key, value|
|
81
|
+
values[key_without_namespace(key)] = value
|
82
|
+
end
|
83
|
+
rescue NetworkError => e
|
84
|
+
Dalli.logger.debug { e.message }
|
85
|
+
Dalli.logger.debug { "results from this server will be missing" }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
values
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def fetch(key, ttl=nil, options=nil)
|
93
|
+
ttl ||= @options[:expires_in]
|
94
|
+
val = get(key, options)
|
95
|
+
if val.nil? && block_given?
|
96
|
+
val = yield
|
97
|
+
add(key, val, ttl, options)
|
98
|
+
end
|
99
|
+
val
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# compare and swap values using optimistic locking.
|
104
|
+
# Fetch the existing value for key.
|
105
|
+
# If it exists, yield the value to the block.
|
106
|
+
# Add the block's return value as the new value for the key.
|
107
|
+
# Add will fail if someone else changed the value.
|
108
|
+
#
|
109
|
+
# Returns:
|
110
|
+
# - nil if the key did not exist.
|
111
|
+
# - false if the value was changed by someone else.
|
112
|
+
# - true if the value was successfully updated.
|
113
|
+
def cas(key, ttl=nil, options=nil, &block)
|
114
|
+
ttl ||= @options[:expires_in]
|
115
|
+
(value, cas) = perform(:cas, key)
|
116
|
+
value = (!value || value == 'Not found') ? nil : value
|
117
|
+
if value
|
118
|
+
newvalue = block.call(value)
|
119
|
+
perform(:add, key, newvalue, ttl, cas, options)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def set(key, value, ttl=nil, options=nil)
|
124
|
+
raise "Invalid API usage, please require 'dalli/memcache-client' for compatibility, see Upgrade.md" if options == true
|
125
|
+
ttl ||= @options[:expires_in]
|
126
|
+
perform(:set, key, value, ttl, options)
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Conditionally add a key/value pair, if the key does not already exist
|
131
|
+
# on the server. Returns true if the operation succeeded.
|
132
|
+
def add(key, value, ttl=nil, options=nil)
|
133
|
+
ttl ||= @options[:expires_in]
|
134
|
+
perform(:add, key, value, ttl, 0, options)
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Conditionally add a key/value pair, only if the key already exists
|
139
|
+
# on the server. Returns true if the operation succeeded.
|
140
|
+
def replace(key, value, ttl=nil, options=nil)
|
141
|
+
ttl ||= @options[:expires_in]
|
142
|
+
perform(:replace, key, value, ttl, options)
|
143
|
+
end
|
144
|
+
|
145
|
+
def delete(key)
|
146
|
+
perform(:delete, key)
|
147
|
+
end
|
148
|
+
|
149
|
+
def append(key, value)
|
150
|
+
perform(:append, key, value.to_s)
|
151
|
+
end
|
152
|
+
|
153
|
+
def prepend(key, value)
|
154
|
+
perform(:prepend, key, value.to_s)
|
155
|
+
end
|
156
|
+
|
157
|
+
def flush(delay=0)
|
158
|
+
time = -delay
|
159
|
+
ring.servers.map { |s| s.request(:flush, time += delay) }
|
160
|
+
end
|
161
|
+
|
162
|
+
# deprecated, please use #flush.
|
163
|
+
alias_method :flush_all, :flush
|
164
|
+
|
165
|
+
##
|
166
|
+
# Incr adds the given amount to the counter on the memcached server.
|
167
|
+
# Amt must be a positive value.
|
168
|
+
#
|
169
|
+
# memcached counters are unsigned and cannot hold negative values. Calling
|
170
|
+
# decr on a counter which is 0 will just return 0.
|
171
|
+
#
|
172
|
+
# If default is nil, the counter must already exist or the operation
|
173
|
+
# will fail and will return nil. Otherwise this method will return
|
174
|
+
# the new value for the counter.
|
175
|
+
def incr(key, amt=1, ttl=nil, default=nil)
|
176
|
+
raise ArgumentError, "Positive values only: #{amt}" if amt < 0
|
177
|
+
ttl ||= @options[:expires_in]
|
178
|
+
perform(:incr, key, amt, ttl, default)
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Decr subtracts the given amount from the counter on the memcached server.
|
183
|
+
# Amt must be a positive value.
|
184
|
+
#
|
185
|
+
# memcached counters are unsigned and cannot hold negative values. Calling
|
186
|
+
# decr on a counter which is 0 will just return 0.
|
187
|
+
#
|
188
|
+
# If default is nil, the counter must already exist or the operation
|
189
|
+
# will fail and will return nil. Otherwise this method will return
|
190
|
+
# the new value for the counter.
|
191
|
+
def decr(key, amt=1, ttl=nil, default=nil)
|
192
|
+
raise ArgumentError, "Positive values only: #{amt}" if amt < 0
|
193
|
+
ttl ||= @options[:expires_in]
|
194
|
+
perform(:decr, key, amt, ttl, default)
|
195
|
+
end
|
196
|
+
|
197
|
+
##
|
198
|
+
# Collect the stats for each server.
|
199
|
+
# Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
|
200
|
+
def stats
|
201
|
+
values = {}
|
202
|
+
ring.servers.each do |server|
|
203
|
+
values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:stats) : nil
|
204
|
+
end
|
205
|
+
values
|
206
|
+
end
|
207
|
+
|
208
|
+
##
|
209
|
+
# Close our connection to each server.
|
210
|
+
# If you perform another operation after this, the connections will be re-established.
|
211
|
+
def close
|
212
|
+
if @ring
|
213
|
+
@ring.servers.map { |s| s.close }
|
214
|
+
@ring = nil
|
215
|
+
end
|
216
|
+
end
|
217
|
+
alias_method :reset, :close
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
def ring
|
222
|
+
@ring ||= Dalli::Ring.new(
|
223
|
+
Array(@servers).map do |s|
|
224
|
+
Dalli::Server.new(s, @options)
|
225
|
+
end, @options
|
226
|
+
)
|
227
|
+
end
|
228
|
+
|
229
|
+
def env_servers
|
230
|
+
ENV['MEMCACHE_SERVERS'] ? ENV['MEMCACHE_SERVERS'].split(',') : nil
|
231
|
+
end
|
232
|
+
|
233
|
+
# Chokepoint method for instrumentation
|
234
|
+
def perform(op, key, *args)
|
235
|
+
key = key.to_s
|
236
|
+
validate_key(key)
|
237
|
+
key = key_with_namespace(key)
|
238
|
+
begin
|
239
|
+
server = ring.server_for_key(key)
|
240
|
+
server.request(op, key, *args)
|
241
|
+
rescue NetworkError => e
|
242
|
+
Dalli.logger.debug { e.message }
|
243
|
+
Dalli.logger.debug { "retrying request with new server" }
|
244
|
+
retry
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def validate_key(key)
|
249
|
+
raise ArgumentError, "illegal character in key #{key}" if key.respond_to?(:ascii_only?) && !key.ascii_only?
|
250
|
+
raise ArgumentError, "illegal character in key #{key}" if key =~ /\s/
|
251
|
+
raise ArgumentError, "illegal character in key #{key}" if key =~ /[\x00-\x20\x80-\xFF]/
|
252
|
+
raise ArgumentError, "key cannot be blank" if key.nil? || key.strip.size == 0
|
253
|
+
raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
|
254
|
+
end
|
255
|
+
|
256
|
+
def key_with_namespace(key)
|
257
|
+
@options[:namespace] ? "#{@options[:namespace]}:#{key}" : key
|
258
|
+
end
|
259
|
+
|
260
|
+
def key_without_namespace(key)
|
261
|
+
@options[:namespace] ? key.gsub(%r(\A#{@options[:namespace]}:), '') : key
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Dalli::Client
|
2
|
+
|
3
|
+
module MemcacheClientCompatibility
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
Dalli.logger.error("Starting Dalli in memcache-client compatibility mode")
|
7
|
+
super(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def set(key, value, ttl = nil, options = nil)
|
11
|
+
if options == true || options == false
|
12
|
+
Dalli.logger.error("Dalli: please use set(key, value, ttl, :raw => boolean): #{caller[0]}")
|
13
|
+
options = { :raw => options }
|
14
|
+
end
|
15
|
+
super(key, value, ttl, options) ? "STORED\r\n" : "NOT_STORED\r\n"
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(key, value, ttl = nil, options = nil)
|
20
|
+
if options == true || options == false
|
21
|
+
Dalli.logger.error("Dalli: please use add(key, value, ttl, :raw => boolean): #{caller[0]}")
|
22
|
+
options = { :raw => options }
|
23
|
+
end
|
24
|
+
super(key, value, ttl, options) ? "STORED\r\n" : "NOT_STORED\r\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
def replace(key, value, ttl = nil, options = nil)
|
28
|
+
if options == true || options == false
|
29
|
+
Dalli.logger.error("Dalli: please use replace(key, value, ttl, :raw => boolean): #{caller[0]}")
|
30
|
+
options = { :raw => options }
|
31
|
+
end
|
32
|
+
super(key, value, ttl, options) ? "STORED\r\n" : "NOT_STORED\r\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Dalli does not unmarshall data that does not have the marshalled flag set so we need
|
36
|
+
# to unmarshall manually any marshalled data originally put in memcached by memcache-client.
|
37
|
+
# Peek at the data and see if it looks marshalled.
|
38
|
+
def get(key, options = nil)
|
39
|
+
value = super(key, options)
|
40
|
+
if value && value.is_a?(String) && !options && value.size > 2 &&
|
41
|
+
bytes = value.unpack('cc') && bytes[0] == 4 && bytes[1] == 8
|
42
|
+
return Marshal.load(value) rescue value
|
43
|
+
end
|
44
|
+
value
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(key)
|
48
|
+
super(key) ? "DELETED\r\n" : "NOT_DELETED\r\n"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Dalli::Client.compatibility_mode = true
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'monitor'
|
3
|
+
|
4
|
+
module Dalli
|
5
|
+
|
6
|
+
# Make Dalli threadsafe by using a lock around all
|
7
|
+
# public server methods.
|
8
|
+
#
|
9
|
+
# Dalli::Server.extend(Dalli::Threadsafe)
|
10
|
+
#
|
11
|
+
module Threadsafe
|
12
|
+
def request(op, *args)
|
13
|
+
lock.synchronize do
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def alive?
|
19
|
+
lock.synchronize do
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
lock.synchronize do
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def lock!
|
31
|
+
lock.mon_enter
|
32
|
+
end
|
33
|
+
|
34
|
+
def unlock!
|
35
|
+
lock.mon_exit
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def lock
|
40
|
+
@lock ||= Monitor.new
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/lib/dalli/ring.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module Dalli
|
5
|
+
class Ring
|
6
|
+
POINTS_PER_SERVER = 160 # this is the default in libmemcached
|
7
|
+
|
8
|
+
attr_accessor :servers, :continuum
|
9
|
+
|
10
|
+
def initialize(servers, options)
|
11
|
+
@servers = servers
|
12
|
+
@continuum = nil
|
13
|
+
if servers.size > 1
|
14
|
+
total_weight = servers.inject(0) { |memo, srv| memo + srv.weight }
|
15
|
+
continuum = []
|
16
|
+
servers.each do |server|
|
17
|
+
entry_count_for(server, servers.size, total_weight).times do |idx|
|
18
|
+
hash = Digest::SHA1.hexdigest("#{server.hostname}:#{server.port}:#{idx}")
|
19
|
+
value = Integer("0x#{hash[0..7]}")
|
20
|
+
continuum << Dalli::Ring::Entry.new(value, server)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
@continuum = continuum.sort { |a, b| a.value <=> b.value }
|
24
|
+
end
|
25
|
+
|
26
|
+
threadsafe! unless options[:threadsafe] == false
|
27
|
+
@failover = options[:failover] != false
|
28
|
+
end
|
29
|
+
|
30
|
+
def server_for_key(key)
|
31
|
+
if @continuum
|
32
|
+
hkey = hash_for(key)
|
33
|
+
20.times do |try|
|
34
|
+
entryidx = self.class.binary_search(@continuum, hkey)
|
35
|
+
server = @continuum[entryidx].server
|
36
|
+
return server if server.alive?
|
37
|
+
break unless @failover
|
38
|
+
hkey = hash_for("#{try}#{key}")
|
39
|
+
end
|
40
|
+
else
|
41
|
+
server = @servers.first
|
42
|
+
return server if server && server.alive?
|
43
|
+
end
|
44
|
+
|
45
|
+
raise Dalli::RingError, "No server available"
|
46
|
+
end
|
47
|
+
|
48
|
+
def lock
|
49
|
+
@servers.each { |s| s.lock! }
|
50
|
+
begin
|
51
|
+
return yield
|
52
|
+
ensure
|
53
|
+
@servers.each { |s| s.unlock! }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def threadsafe!
|
60
|
+
@servers.each do |s|
|
61
|
+
s.extend(Dalli::Threadsafe)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def hash_for(key)
|
66
|
+
Zlib.crc32(key)
|
67
|
+
end
|
68
|
+
|
69
|
+
def entry_count_for(server, total_servers, total_weight)
|
70
|
+
((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
|
71
|
+
end
|
72
|
+
|
73
|
+
# Find the closest index in the Ring with value <= the given value
|
74
|
+
def self.binary_search(ary, value)
|
75
|
+
upper = ary.size - 1
|
76
|
+
lower = 0
|
77
|
+
idx = 0
|
78
|
+
|
79
|
+
while (lower <= upper) do
|
80
|
+
idx = (lower + upper) / 2
|
81
|
+
comp = ary[idx].value <=> value
|
82
|
+
|
83
|
+
if comp == 0
|
84
|
+
return idx
|
85
|
+
elsif comp > 0
|
86
|
+
upper = idx - 1
|
87
|
+
else
|
88
|
+
lower = idx + 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
return upper
|
92
|
+
end
|
93
|
+
|
94
|
+
class Entry
|
95
|
+
attr_reader :value
|
96
|
+
attr_reader :server
|
97
|
+
|
98
|
+
def initialize(val, srv)
|
99
|
+
@value = val
|
100
|
+
@server = srv
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|