memcache-client 1.7.7 → 1.7.8
Sign up to get free protection for your applications and to get access to all the features.
- data/FAQ.rdoc +1 -1
- data/History.rdoc +17 -11
- data/README.rdoc +1 -1
- data/Rakefile +4 -5
- data/VERSION.yml +2 -2
- data/lib/memcache.rb +18 -10
- data/lib/memcache_util.rb +2 -2
- data/test/test_benchmark.rb +10 -10
- data/test/test_mem_cache.rb +29 -21
- metadata +3 -7
- data/lib/continuum_native.rbc +0 -362
- data/lib/memcache.rbc +0 -18734
- data/test/test_benchmark.rbc +0 -2524
- data/test/test_mem_cache.rbc +0 -26058
data/FAQ.rdoc
CHANGED
@@ -27,5 +27,5 @@ You can increase the timeout or disable them completely with the following confi
|
|
27
27
|
|
28
28
|
The latest version of memcached-client is anywhere from 33% to 100% slower than memcached in various benchmarks. Keep in mind this means that 10,000 get requests take 1.8 sec instead of 1.2 seconds.
|
29
29
|
In practice, memcache-client is unlikely to be a bottleneck in your system but there is always going
|
30
|
-
to be an overhead to pure Ruby. memcache-client does have the advantage of built-in integration into
|
30
|
+
to be an overhead to pure Ruby. memcache-client does have the advantage of built-in integration into
|
31
31
|
Rails and should work on non-MRI platforms: JRuby, MacRuby, etc.
|
data/History.rdoc
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
+
= 1.7.8 (2010-02-03)
|
2
|
+
|
3
|
+
* Fix issue where autofix_keys logic did not account for namespace length. (menno)
|
4
|
+
* Fix issue when using memcache-client without rubygems. (anvar)
|
5
|
+
* Fix issue when using the cas method with raw=true (Takahiro Kikumoto)
|
6
|
+
|
1
7
|
= 1.7.7 (2009-11-24)
|
2
8
|
|
3
|
-
* Fix invalid delete request in memcached 1.4.x. The expiry parameter to MemCache#delete is
|
9
|
+
* Fix invalid delete request in memcached 1.4.x. The expiry parameter to MemCache#delete is
|
4
10
|
now ignored as memcached 1.4.x has dropped support for this feature.
|
5
11
|
|
6
12
|
= 1.7.6 (2009-11-03)
|
@@ -39,15 +45,15 @@
|
|
39
45
|
|
40
46
|
= 1.7.1 (2009-03-28)
|
41
47
|
|
42
|
-
* Performance optimizations:
|
43
|
-
* Rely on higher performance operating system socket timeouts for low-level socket
|
48
|
+
* Performance optimizations:
|
49
|
+
* Rely on higher performance operating system socket timeouts for low-level socket
|
44
50
|
read/writes where possible, instead of the (slower) SystemTimer or (slowest,
|
45
51
|
unreliable) Timeout libraries.
|
46
|
-
* the native binary search is back! The recent performance tuning made the binary search
|
47
|
-
a bottleneck again so it had to return. It uses RubyInline to compile the native extension and
|
52
|
+
* the native binary search is back! The recent performance tuning made the binary search
|
53
|
+
a bottleneck again so it had to return. It uses RubyInline to compile the native extension and
|
48
54
|
silently falls back to pure Ruby if anything fails. Make sure you run:
|
49
55
|
`gem install RubyInline` if you want ultimate performance.
|
50
|
-
* the changes make memcache-client 100% faster than 1.7.0 in my performance test on Ruby 1.8.6:
|
56
|
+
* the changes make memcache-client 100% faster than 1.7.0 in my performance test on Ruby 1.8.6:
|
51
57
|
15 sec -> 8 sec.
|
52
58
|
* Fix several logging issues.
|
53
59
|
|
@@ -58,7 +64,7 @@
|
|
58
64
|
- append
|
59
65
|
- prepend
|
60
66
|
- replace
|
61
|
-
|
67
|
+
|
62
68
|
Append and prepend only work with raw data since it makes no sense to concatenate two Marshalled
|
63
69
|
values together. The cas functionality should be considered a prototype. Since I don't have an
|
64
70
|
application which uses +cas+, I'm not sure what semantic sugar the API should provide. Should it
|
@@ -119,17 +125,17 @@ and work on JRuby and Ruby 1.8.5 when the native code fails to compile.
|
|
119
125
|
|
120
126
|
= 1.6.1 (2009-01-28)
|
121
127
|
|
122
|
-
* Add option to disable socket timeout support. Socket timeout has a significant performance
|
123
|
-
penalty (approx 3x slower than without in Ruby 1.8.6). You can turn off the timeouts if you
|
128
|
+
* Add option to disable socket timeout support. Socket timeout has a significant performance
|
129
|
+
penalty (approx 3x slower than without in Ruby 1.8.6). You can turn off the timeouts if you
|
124
130
|
need absolute performance, but by default timeouts are enabled. The performance
|
125
131
|
penalty is much lower in Ruby 1.8.7, 1.9 and JRuby. (mperham)
|
126
132
|
|
127
|
-
* Add option to disable server failover. Failover can lead to "split-brain" caches that
|
133
|
+
* Add option to disable server failover. Failover can lead to "split-brain" caches that
|
128
134
|
return stale data. (mperham)
|
129
135
|
|
130
136
|
* Implement continuum binary search in native code for performance reasons. Pure ruby
|
131
137
|
is available for platforms like JRuby or Rubinius which can't use C extensions. (mperham)
|
132
|
-
|
138
|
+
|
133
139
|
* Fix #add with raw=true (iamaleksey)
|
134
140
|
|
135
141
|
= 1.6.0
|
data/README.rdoc
CHANGED
@@ -39,7 +39,7 @@ gem installed.
|
|
39
39
|
|
40
40
|
== Questions?
|
41
41
|
|
42
|
-
memcache-client is maintained by Mike Perham and was originally written by Bob Cottrell,
|
42
|
+
memcache-client is maintained by Mike Perham and was originally written by Bob Cottrell,
|
43
43
|
Eric Hodel and the seattle.rb crew.
|
44
44
|
|
45
45
|
Email:: mailto:mperham@gmail.com
|
data/Rakefile
CHANGED
@@ -14,18 +14,17 @@ begin
|
|
14
14
|
s.has_rdoc = true
|
15
15
|
s.files = FileList["[A-Z]*", "{lib,test}/**/*", 'performance.txt']
|
16
16
|
s.test_files = FileList["test/test_*.rb"]
|
17
|
-
s.rubyforge_project = 'seattlerb'
|
18
17
|
end
|
19
|
-
|
18
|
+
Jeweler::GemcutterTasks.new
|
20
19
|
rescue LoadError
|
21
20
|
puts "Jeweler not available. Install it for jeweler-related tasks with: sudo gem install jeweler"
|
22
21
|
end
|
23
22
|
|
24
23
|
|
25
24
|
Rake::RDocTask.new do |rd|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
rd.main = "README.rdoc"
|
26
|
+
rd.rdoc_files.include("README.rdoc", "FAQ.rdoc", "History.rdoc", "lib/memcache.rb")
|
27
|
+
rd.rdoc_dir = 'doc'
|
29
28
|
end
|
30
29
|
|
31
30
|
Rake::TestTask.new do |t|
|
data/VERSION.yml
CHANGED
data/lib/memcache.rb
CHANGED
@@ -3,6 +3,7 @@ $TESTING = defined?($TESTING) && $TESTING
|
|
3
3
|
require 'socket'
|
4
4
|
require 'thread'
|
5
5
|
require 'zlib'
|
6
|
+
require 'yaml'
|
6
7
|
require 'digest/sha1'
|
7
8
|
require 'net/protocol'
|
8
9
|
|
@@ -82,13 +83,13 @@ class MemCache
|
|
82
83
|
# The hash is only used on keys longer than 250 characters, or containing spaces,
|
83
84
|
# to avoid impacting performance unnecesarily.
|
84
85
|
#
|
85
|
-
# In theory, your code should generate correct keys when calling memcache,
|
86
|
+
# In theory, your code should generate correct keys when calling memcache,
|
86
87
|
# so it's your responsibility and you should try to fix this problem at its source.
|
87
88
|
#
|
88
89
|
# But if that's not possible, enable this option and memcache-client will give you a hand.
|
89
|
-
|
90
|
+
|
90
91
|
attr_reader :autofix_keys
|
91
|
-
|
92
|
+
|
92
93
|
##
|
93
94
|
# The servers this client talks to. Play at your own peril.
|
94
95
|
|
@@ -136,7 +137,7 @@ class MemCache
|
|
136
137
|
# set/add/delete/incr/decr significantly.
|
137
138
|
# [:check_size] Raises a MemCacheError if the value to be set is greater than 1 MB, which
|
138
139
|
# is the maximum key size for the standard memcached server. Defaults to true.
|
139
|
-
# [:autofix_keys] If a key is longer than 250 characters or contains spaces,
|
140
|
+
# [:autofix_keys] If a key is longer than 250 characters or contains spaces,
|
140
141
|
# use an SHA1 hash instead, to prevent collisions on truncated keys.
|
141
142
|
# Other options are ignored.
|
142
143
|
|
@@ -260,7 +261,7 @@ class MemCache
|
|
260
261
|
end
|
261
262
|
|
262
263
|
##
|
263
|
-
# Performs a +get+ with the given +key+. If
|
264
|
+
# Performs a +get+ with the given +key+. If
|
264
265
|
# the value does not exist and a block was given,
|
265
266
|
# the block will be called and the result saved via +add+.
|
266
267
|
#
|
@@ -405,7 +406,7 @@ class MemCache
|
|
405
406
|
(value, token) = gets(key, raw)
|
406
407
|
return nil unless value
|
407
408
|
updated = yield value
|
408
|
-
value = Marshal.dump
|
409
|
+
value = raw ? updated : Marshal.dump(updated)
|
409
410
|
|
410
411
|
with_server(key) do |server, cache_key|
|
411
412
|
logger.debug { "cas #{key} to #{server.inspect}: #{value.to_s.size}" } if logger
|
@@ -451,7 +452,7 @@ class MemCache
|
|
451
452
|
end
|
452
453
|
end
|
453
454
|
end
|
454
|
-
|
455
|
+
|
455
456
|
##
|
456
457
|
# Add +key+ to the cache with value +value+ that expires in +expiry+
|
457
458
|
# seconds, but only if +key+ already exists in the cache.
|
@@ -667,17 +668,24 @@ class MemCache
|
|
667
668
|
# requested.
|
668
669
|
|
669
670
|
def make_cache_key(key)
|
670
|
-
if @autofix_keys
|
671
|
+
if @autofix_keys && (key =~ /\s/ || key_length(key) > 250)
|
671
672
|
key = "#{Digest::SHA1.hexdigest(key)}-autofixed"
|
672
673
|
end
|
673
674
|
|
674
|
-
if namespace.nil?
|
675
|
+
if namespace.nil?
|
675
676
|
key
|
676
677
|
else
|
677
678
|
"#{@namespace}#{@namespace_separator}#{key}"
|
678
679
|
end
|
679
680
|
end
|
680
681
|
|
682
|
+
##
|
683
|
+
# Calculate length of the key, including the namespace and namespace-separator.
|
684
|
+
|
685
|
+
def key_length(key)
|
686
|
+
key.length + (namespace.nil? ? 0 : ( namespace.length + (@namespace_separator.nil? ? 0 : @namespace_separator.length) ) )
|
687
|
+
end
|
688
|
+
|
681
689
|
##
|
682
690
|
# Returns an interoperable hash value for +key+. (I think, docs are
|
683
691
|
# sketchy for down servers).
|
@@ -705,7 +713,7 @@ class MemCache
|
|
705
713
|
break unless failover
|
706
714
|
hkey = hash_for "#{try}#{key}"
|
707
715
|
end
|
708
|
-
|
716
|
+
|
709
717
|
raise MemCacheError, "No servers available"
|
710
718
|
end
|
711
719
|
|
data/lib/memcache_util.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
##
|
2
2
|
# A utility wrapper around the MemCache client to simplify cache access. All
|
3
3
|
# methods silently ignore MemCache errors.
|
4
|
-
#
|
4
|
+
#
|
5
5
|
# This API is deprecated, please use the Rails.cache API or your own wrapper API
|
6
6
|
# around MemCache.
|
7
7
|
|
8
8
|
module Cache
|
9
|
-
|
9
|
+
|
10
10
|
##
|
11
11
|
# Try to return a logger object that does not rely
|
12
12
|
# on ActiveRecord for logging.
|
data/test/test_benchmark.rb
CHANGED
@@ -17,7 +17,7 @@ class TestBenchmark < Test::Unit::TestCase
|
|
17
17
|
# which is a constant penalty that both clients have to pay
|
18
18
|
@value = []
|
19
19
|
@marshalled = Marshal.dump(@value)
|
20
|
-
|
20
|
+
|
21
21
|
@opts = [
|
22
22
|
['127.0.0.1:11211', 'localhost:11211'],
|
23
23
|
{
|
@@ -31,16 +31,16 @@ class TestBenchmark < Test::Unit::TestCase
|
|
31
31
|
@key3 = "Long"*40
|
32
32
|
@key4 = "Medium"*8
|
33
33
|
# 5 and 6 are only used for multiget miss test
|
34
|
-
@key5 = "Medium2"*8
|
35
|
-
@key6 = "Long3"*40
|
34
|
+
@key5 = "Medium2"*8
|
35
|
+
@key6 = "Long3"*40
|
36
36
|
end
|
37
37
|
|
38
38
|
def test_benchmark
|
39
39
|
Benchmark.bm(31) do |x|
|
40
|
-
|
40
|
+
|
41
41
|
n = 2500
|
42
42
|
# n = 1000
|
43
|
-
|
43
|
+
|
44
44
|
@m = MemCache.new(*@opts)
|
45
45
|
x.report("set:plain:memcache-client") do
|
46
46
|
n.times do
|
@@ -52,7 +52,7 @@ class TestBenchmark < Test::Unit::TestCase
|
|
52
52
|
@m.set @key3, @marshalled, 0, true
|
53
53
|
end
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
@m = MemCache.new(*@opts)
|
57
57
|
x.report("set:ruby:memcache-client") do
|
58
58
|
n.times do
|
@@ -64,7 +64,7 @@ class TestBenchmark < Test::Unit::TestCase
|
|
64
64
|
@m.set @key3, @value
|
65
65
|
end
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
68
|
@m = MemCache.new(*@opts)
|
69
69
|
x.report("get:plain:memcache-client") do
|
70
70
|
n.times do
|
@@ -76,7 +76,7 @@ class TestBenchmark < Test::Unit::TestCase
|
|
76
76
|
@m.get @key3, true
|
77
77
|
end
|
78
78
|
end
|
79
|
-
|
79
|
+
|
80
80
|
@m = MemCache.new(*@opts)
|
81
81
|
x.report("get:ruby:memcache-client") do
|
82
82
|
n.times do
|
@@ -96,7 +96,7 @@ class TestBenchmark < Test::Unit::TestCase
|
|
96
96
|
@m.get_multi @key1, @key2, @key3, @key4, @key5, @key6
|
97
97
|
end
|
98
98
|
end
|
99
|
-
|
99
|
+
|
100
100
|
@m = MemCache.new(*@opts)
|
101
101
|
x.report("missing:ruby:memcache-client") do
|
102
102
|
n.times do
|
@@ -108,7 +108,7 @@ class TestBenchmark < Test::Unit::TestCase
|
|
108
108
|
begin @m.get @key3; rescue; end
|
109
109
|
end
|
110
110
|
end
|
111
|
-
|
111
|
+
|
112
112
|
@m = MemCache.new(*@opts)
|
113
113
|
x.report("mixed:ruby:memcache-client") do
|
114
114
|
n.times do
|
data/test/test_mem_cache.rb
CHANGED
@@ -19,7 +19,7 @@ class MemCache
|
|
19
19
|
|
20
20
|
attr_writer :namespace
|
21
21
|
attr_writer :autofix_keys
|
22
|
-
|
22
|
+
|
23
23
|
end
|
24
24
|
|
25
25
|
class FakeSocket
|
@@ -54,11 +54,11 @@ class Test::Unit::TestCase
|
|
54
54
|
assert true
|
55
55
|
end
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
def memcached_running?
|
59
59
|
TCPSocket.new('localhost', 11211) rescue false
|
60
60
|
end
|
61
|
-
|
61
|
+
|
62
62
|
def xprofile(name, &block)
|
63
63
|
a = Time.now
|
64
64
|
block.call
|
@@ -76,7 +76,7 @@ class Test::Unit::TestCase
|
|
76
76
|
end
|
77
77
|
time
|
78
78
|
end
|
79
|
-
|
79
|
+
|
80
80
|
end
|
81
81
|
|
82
82
|
class FakeServer
|
@@ -168,7 +168,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
168
168
|
assert same_count > 700
|
169
169
|
end
|
170
170
|
end
|
171
|
-
|
171
|
+
|
172
172
|
def test_get_multi_with_server_failure
|
173
173
|
@cache = MemCache.new 'localhost:1', :namespace => 'my_namespace', :logger => nil #Logger.new(STDOUT)
|
174
174
|
s1 = FakeServer.new
|
@@ -211,11 +211,11 @@ class TestMemCache < Test::Unit::TestCase
|
|
211
211
|
assert s1.alive?
|
212
212
|
assert !s2.alive?
|
213
213
|
end
|
214
|
-
|
214
|
+
|
215
215
|
def test_cache_get_without_failover
|
216
216
|
s1 = FakeServer.new
|
217
217
|
s2 = FakeServer.new
|
218
|
-
|
218
|
+
|
219
219
|
s1.socket.data.write "VALUE foo 0 14\r\n\004\b\"\0170123456789\r\n"
|
220
220
|
s1.socket.data.rewind
|
221
221
|
s2.socket.data.write "bogus response\r\nbogus response\r\n"
|
@@ -336,7 +336,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
336
336
|
def test_multithread_error
|
337
337
|
server = FakeServer.new
|
338
338
|
server.multithread = false
|
339
|
-
|
339
|
+
|
340
340
|
@cache = MemCache.new(['localhost:1'], :multithread => false)
|
341
341
|
|
342
342
|
server.socket.data.write "bogus response\r\nbogus response\r\n"
|
@@ -489,7 +489,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
489
489
|
value = @cache.fetch('key', 1)
|
490
490
|
assert_equal nil, value
|
491
491
|
end
|
492
|
-
|
492
|
+
|
493
493
|
def test_fetch_miss
|
494
494
|
server = FakeServer.new
|
495
495
|
server.socket.data.write "END\r\n"
|
@@ -725,29 +725,37 @@ class TestMemCache < Test::Unit::TestCase
|
|
725
725
|
hash = Digest::SHA1.hexdigest(key)
|
726
726
|
@cache.namespace = nil
|
727
727
|
assert_equal key, @cache.make_cache_key(key)
|
728
|
+
end
|
728
729
|
|
730
|
+
def test_make_cache_key_with_autofix
|
729
731
|
@cache.autofix_keys = true
|
730
732
|
|
731
733
|
@cache.namespace = "my_namespace"
|
732
734
|
assert_equal 'my_namespace:key', @cache.make_cache_key('key')
|
733
735
|
@cache.namespace = nil
|
734
736
|
assert_equal 'key', @cache.make_cache_key('key')
|
735
|
-
|
737
|
+
|
736
738
|
key = "keys with more than two hundred and fifty characters can cause problems, because they get truncated and start colliding with each other. It's not a common occurrence, but when it happens is very hard to debug. the autofix option takes care of that for you"
|
737
739
|
hash = Digest::SHA1.hexdigest(key)
|
738
740
|
@cache.namespace = "my_namespace"
|
739
741
|
assert_equal "my_namespace:#{hash}-autofixed", @cache.make_cache_key(key)
|
740
742
|
@cache.namespace = nil
|
741
743
|
assert_equal "#{hash}-autofixed", @cache.make_cache_key(key)
|
742
|
-
|
744
|
+
|
743
745
|
key = "a short key with spaces"
|
744
746
|
hash = Digest::SHA1.hexdigest(key)
|
745
747
|
@cache.namespace = "my_namespace"
|
746
748
|
assert_equal "my_namespace:#{hash}-autofixed", @cache.make_cache_key(key)
|
747
749
|
@cache.namespace = nil
|
748
750
|
assert_equal "#{hash}-autofixed", @cache.make_cache_key(key)
|
751
|
+
|
752
|
+
# namespace + separator + key > 250
|
753
|
+
key = 'k' * 240
|
754
|
+
hash = Digest::SHA1.hexdigest(key)
|
755
|
+
@cache.namespace = 'n' * 10
|
756
|
+
assert_equal "#{@cache.namespace}:#{hash}-autofixed", @cache.make_cache_key(key)
|
749
757
|
end
|
750
|
-
|
758
|
+
|
751
759
|
def test_servers
|
752
760
|
server = FakeServer.new
|
753
761
|
@cache.servers = []
|
@@ -864,7 +872,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
864
872
|
@cache.servers << server
|
865
873
|
|
866
874
|
@cache.prepend 'key', 'value'
|
867
|
-
|
875
|
+
|
868
876
|
dumped = Marshal.dump('value')
|
869
877
|
|
870
878
|
expected = "prepend my_namespace:key 0 0 5\r\nvalue\r\n"
|
@@ -879,7 +887,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
879
887
|
@cache.servers << server
|
880
888
|
|
881
889
|
@cache.append 'key', 'value'
|
882
|
-
|
890
|
+
|
883
891
|
expected = "append my_namespace:key 0 0 5\r\nvalue\r\n"
|
884
892
|
assert_equal expected, server.socket.written.string
|
885
893
|
end
|
@@ -892,7 +900,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
892
900
|
@cache.servers << server
|
893
901
|
|
894
902
|
@cache.replace 'key', 'value', 150
|
895
|
-
|
903
|
+
|
896
904
|
dumped = Marshal.dump('value')
|
897
905
|
|
898
906
|
expected = "replace my_namespace:key 0 150 #{dumped.length}\r\n#{dumped}\r\n"
|
@@ -907,7 +915,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
907
915
|
@cache.servers << server
|
908
916
|
|
909
917
|
@cache.add 'key', 'value'
|
910
|
-
|
918
|
+
|
911
919
|
dumped = Marshal.dump('value')
|
912
920
|
|
913
921
|
expected = "add my_namespace:key 0 0 #{dumped.length}\r\n#{dumped}\r\n"
|
@@ -982,9 +990,9 @@ class TestMemCache < Test::Unit::TestCase
|
|
982
990
|
server = FakeServer.new
|
983
991
|
@cache.servers = []
|
984
992
|
@cache.servers << server
|
985
|
-
|
993
|
+
|
986
994
|
@cache.delete 'key'
|
987
|
-
|
995
|
+
|
988
996
|
expected = "delete my_namespace:key\r\n"
|
989
997
|
assert_equal expected, server.socket.written.string
|
990
998
|
end
|
@@ -993,9 +1001,9 @@ class TestMemCache < Test::Unit::TestCase
|
|
993
1001
|
server = FakeServer.new
|
994
1002
|
@cache.servers = []
|
995
1003
|
@cache.servers << server
|
996
|
-
|
1004
|
+
|
997
1005
|
@cache.delete 'key', 300
|
998
|
-
|
1006
|
+
|
999
1007
|
expected = "delete my_namespace:key\r\n"
|
1000
1008
|
assert_equal expected, server.socket.written.string
|
1001
1009
|
end
|
@@ -1042,7 +1050,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
1042
1050
|
|
1043
1051
|
assert_match(/flush_all\r\n/, socket.written.string)
|
1044
1052
|
end
|
1045
|
-
|
1053
|
+
|
1046
1054
|
def test_flush_all_for_real
|
1047
1055
|
requirement(memcached_running?, 'A real memcached server must be running for testing flush_all') do
|
1048
1056
|
cache = MemCache.new "localhost:11211", :namespace => "test_flush_all"
|