pax_global_header00006660000000000000000000000064121226657570014527gustar00rootroot0000000000000052 comment=1b2fcc017c5ca28ffb619fe0cf61e8e5dd52fb19 ruby-remcached-0.4.1/000077500000000000000000000000001212266575700144235ustar00rootroot00000000000000ruby-remcached-0.4.1/.gitignore000066400000000000000000000000061212266575700164070ustar00rootroot00000000000000*# *~ ruby-remcached-0.4.1/README.rst000066400000000000000000000065651212266575700161260ustar00rootroot00000000000000remcached ========= * **Ruby EventMachine memCACHED client implementation** * provides a direct interface to the memcached protocol and its semantics * uses the memcached `binary protocol`_ to reduce parsing overhead on the server side (requires memcached >= 1.3) * supports multiple servers with simple round-robin key hashing (**TODO:** implement the libketama algorithm) in a fault-tolerant way * writing your own abstraction layer is recommended * uses RSpec * partially documented in RDoc-style Callbacks --------- Each request `may` be passed a callback. These are not two-cased (success & failure) EM deferrables, but standard Ruby callbacks. The rationale behind this is that there are no usual success/failure responses, but you will want to evaluate a ``response[:status]`` yourself to check for cache miss, version conflict, network disconnects etc. A callback may be kept if it returns ``:proceed`` to catch multi-response commands such as ``STAT``. remcached has been built with **fault tolerance** in mind: a callback will be called with just ``{:status => Memcached::Errors::DISCONNECTED}`` if the network connection has went away. Thus, you can expect your callback will be called, except of course you're using `quiet` commands. In that case, only a "non-usual response" from the server or a network failure will invoke your block. Multi commands -------------- The technique is described in the `binary protocol`_ spec in section **4.2**. ``Memcached.multi_operation`` will help you exactly with that, sending lots of those `quiet` commands, except for the last, which will be a `normal` command to trigger an acknowledge for all commands. This is of course implemented per-server to accomodate load-balancing. Usage ----- First, pass your memcached servers to the library:: Memcached.servers = %w(localhost localhost:11212 localhost:11213) Note that it won't be connected immediately. Use ``Memcached.usable?`` to check. This however complicates your own code and you can check ``response[:status] == Memcached::Errors::DISCONNECTED`` for network errors in all your response callbacks. Further usage is pretty straight-forward:: Memcached.get(:key => 'Hello') do |response| case response[:status] when Memcached::Errors::NO_ERROR use_cached_value response[:value] # ... when Memcached::Errors::KEY_NOT_FOUND refresh_cache! # ... when Memcached::Errors::DISCONNECTED proceed_uncached # ... else cry_for_help # ... end end end Memcached.set(:key => 'Hello', :value => 'World', :expiration => 600) do |response| case response[:status] when Memcached::Errors::NO_ERROR # That's good when Memcached::Errors::DISCONNECTED # Maybe stop filling the cache for now? else # What could've gone wrong? end end end Multi-commands may require a bit of precaution:: Memcached.multi_get([{:key => 'foo'}, {:key => 'bar'}]) do |responses| # responses is now a hash of Key => Response end It's not guaranteed that any of these keys will be present in the response. Moreover, they may be present even if they are a usual response because the last request is always non-quiet. **HAPPY CACHING!** .. _binary protocol: http://code.google.com/p/memcached/wiki/MemcacheBinaryProtocol ruby-remcached-0.4.1/Rakefile000066400000000000000000000007351212266575700160750ustar00rootroot00000000000000begin require 'jeweler' Jeweler::Tasks.new do |gemspec| gemspec.name = "remcached" gemspec.summary = "Ruby EventMachine memcached client" gemspec.description = gemspec.summary gemspec.email = "astro@spaceboyz.net" gemspec.homepage = "http://github.com/astro/remcached/" gemspec.authors = ["Stephan Maka"] end rescue LoadError puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" end ruby-remcached-0.4.1/VERSION.yml000066400000000000000000000000431212266575700162700ustar00rootroot00000000000000--- :minor: 4 :patch: 1 :major: 0 ruby-remcached-0.4.1/examples/000077500000000000000000000000001212266575700162415ustar00rootroot00000000000000ruby-remcached-0.4.1/examples/fill.rb000066400000000000000000000030351212266575700175150ustar00rootroot00000000000000#!/usr/bin/env ruby # Experimentally determine how many items fit in your memcached # instance. Adjust parameters below for your scenario. BATCH_SIZE = 10000 KEY_SIZE = 26 VALUE_SIZE = 0 $: << File.dirname(__FILE__) + "/../lib" require 'remcached' EM.run do @total = 0 # Action def fill old_total = @total reqs = (1..BATCH_SIZE).map { @total += 1 { :key => sprintf("%0#{KEY_SIZE}X", @total), :value => sprintf("%0#{VALUE_SIZE}X", @total) } } Memcached.multi_add(reqs) do |resps| resps.each do |key,resp| case resp[:status] when Memcached::Errors::NO_ERROR :ok when Memcached::Errors::KEY_EXISTS @total -= 1 else puts "Cannot set #{key}: status=#{resp[:status].inspect}" @total -= 1 end end puts "Added #{@total - old_total}, now: #{@total}" if Memcached.usable? stats = {} Memcached.usable_clients[0].stats do |resp| if resp[:key] != '' stats[resp[:key]] = resp[:value] else puts "Stats: #{stats['bytes']} bytes in #{stats['curr_items']} of #{stats['total_items']} items" end end # Next round: fill else EM.stop end end end # Initialization & start Memcached.servers = %w(localhost) @t = EM::PeriodicTimer.new(0.01) do if Memcached.usable? puts "Connected to server" @t.cancel fill else puts "Waiting for server connection..." end end end ruby-remcached-0.4.1/lib/000077500000000000000000000000001212266575700151715ustar00rootroot00000000000000ruby-remcached-0.4.1/lib/remcached.rb000066400000000000000000000074771212266575700174500ustar00rootroot00000000000000require 'remcached/const' require 'remcached/packet' require 'remcached/client' module Memcached class << self ## # +servers+: Array of host:port strings def servers=(servers) if defined?(@clients) && @clients while client = @clients.shift begin client.close rescue Exception # This is allowed to fail silently end end end @clients = servers.collect { |server| host, port = server.split(':') Client.connect host, (port ? port.to_i : 11211) } end def usable? usable_clients.length > 0 end def usable_clients unless defined?(@clients) && @clients [] else @clients.select { |client| client.connected? } end end def client_for_key(key) usable_clients_ = usable_clients if usable_clients_.empty? nil else h = hash_key(key) % usable_clients_.length usable_clients_[h] end end def hash_key(key) hashed = 0 i = 0 key.each_byte do |b| j = key.length - i - 1 % 4 hashed ^= b << (j * 8) i += 1 end hashed end ## # Memcached operations ## def operation(request_klass, contents, &callback) client = client_for_key(contents[:key]) if client client.send_request request_klass.new(contents), &callback elsif callback callback.call :status => Errors::DISCONNECTED end end def add(contents, &callback) operation Request::Add, contents, &callback end def get(contents, &callback) operation Request::Get, contents, &callback end def set(contents, &callback) operation Request::Set, contents, &callback end def delete(contents, &callback) operation Request::Delete, contents, &callback end ## # Multi operations # ## def multi_operation(request_klass, contents_list, &callback) if contents_list.empty? callback.call [] return self end results = {} # Assemble client connections per keys client_contents = {} contents_list.each do |contents| client = client_for_key(contents[:key]) if client client_contents[client] ||= [] client_contents[client] << contents else puts "no client for #{contents[:key].inspect}" results[contents[:key]] = {:status => Memcached::Errors::DISCONNECTED} end end # send requests and wait for responses per client clients_pending = client_contents.length client_contents.each do |client,contents_list| last_i = contents_list.length - 1 client_results = {} contents_list.each_with_index do |contents,i| if i < last_i request = request_klass::Quiet.new(contents) client.send_request(request) { |response| results[contents[:key]] = response } else # last request for this client request = request_klass.new(contents) client.send_request(request) { |response| results[contents[:key]] = response clients_pending -= 1 if clients_pending < 1 callback.call results end } end end end self end def multi_add(contents_list, &callback) multi_operation Request::Add, contents_list, &callback end def multi_get(contents_list, &callback) multi_operation Request::Get, contents_list, &callback end def multi_set(contents_list, &callback) multi_operation Request::Set, contents_list, &callback end def multi_delete(contents_list, &callback) multi_operation Request::Delete, contents_list, &callback end end end ruby-remcached-0.4.1/lib/remcached/000077500000000000000000000000001212266575700171045ustar00rootroot00000000000000ruby-remcached-0.4.1/lib/remcached/client.rb000066400000000000000000000077431212266575700207220ustar00rootroot00000000000000require 'eventmachine' module Memcached class Connection < EventMachine::Connection def self.connect(host, port=11211, &connect_callback) df = EventMachine::DefaultDeferrable.new df.callback &connect_callback EventMachine.connect(host, port, self) do |me| me.instance_eval { @host, @port = host, port @connect_deferrable = df } end end def connected? @connected end def reconnect @connect_deferrable = EventMachine::DefaultDeferrable.new super @host, @port @connect_deferrable end def post_init @recv_buf = "" @recv_state = :header @connected = false @keepalive_timer = nil end def connection_completed @connected = true @connect_deferrable.succeed(self) @last_receive = Time.now @keepalive_timer = EventMachine::PeriodicTimer.new(1, &method(:keepalive)) end RECONNECT_DELAY = 10 RECONNECT_JITTER = 5 def unbind @keepalive_timer.cancel if @keepalive_timer @connected = false EventMachine::Timer.new(RECONNECT_DELAY + rand(RECONNECT_JITTER), method(:reconnect)) end RECEIVE_TIMEOUT = 15 KEEPALIVE_INTERVAL = 5 def keepalive if @last_receive + RECEIVE_TIMEOUT <= Time.now p :timeout close_connection elsif @last_receive + KEEPALIVE_INTERVAL <= Time.now send_keepalive end end def send_packet(pkt) send_data pkt.to_s end def receive_data(data) @recv_buf += data @last_receive = Time.now done = false while not done if @recv_state == :header && @recv_buf.length >= 24 @received = Response.parse_header(@recv_buf[0..23]) @recv_buf = @recv_buf[24..-1] @recv_state = :body elsif @recv_state == :body && @recv_buf.length >= @received[:total_body_length] @recv_buf = @received.parse_body(@recv_buf) receive_packet(@received) @recv_state = :header else done = true end end end end class Client < Connection def post_init super @opaque_counter = 0 @pending = [] end def unbind super @pending.each do |opaque, callback| callback.call :status => Errors::DISCONNECTED end @pending = [] end def send_request(pkt, &callback) @opaque_counter += 1 @opaque_counter %= 1 << 32 pkt[:opaque] = @opaque_counter send_packet pkt if callback @pending << [@opaque_counter, callback] end end ## # memcached responses possess the same order as their # corresponding requests. Therefore quiet requests that have not # yielded responses will be dropped silently to free memory from # +@pending+ # # When a callback has been fired and returned +:proceed+ without a # succeeding packet, we still keep it referenced around for # commands such as STAT which has multiple response packets. def receive_packet(response) pending_pos = nil pending_callback = nil @pending.each_with_index do |(pending_opaque,pending_cb),i| if response[:opaque] == pending_opaque pending_pos = i pending_callback = pending_cb break end end if pending_pos @pending = @pending[pending_pos..-1] begin if pending_callback.call(response) != :proceed @pending.shift end rescue Exception => e $stderr.puts "#{e.class}: #{e}\n" + e.backtrace.join("\n") end end end def send_keepalive send_request Request::NoOp.new end # Callback will be called multiple times def stats(contents={}, &callback) send_request Request::Stats.new(contents) do |result| callback.call result if result[:status] == Errors::NO_ERROR && result[:key] != '' :proceed end end end end end ruby-remcached-0.4.1/lib/remcached/const.rb000066400000000000000000000021241212266575700205560ustar00rootroot00000000000000module Memcached module Datatypes RAW_BYTES = 0x00 end module Errors NO_ERROR = 0x0000 KEY_NOT_FOUND = 0x0001 KEY_EXISTS = 0x0002 VALUE_TOO_LARGE = 0x0003 INVALID_ARGS = 0x0004 ITEM_NOT_STORED = 0x0005 NON_NUMERIC_VALUE = 0x0006 DISCONNECTED = 0xffff end module Commands GET = 0x00 SET = 0x01 ADD = 0x02 REPLACE = 0x03 DELETE = 0x04 INCREMENT = 0x05 DECREMENT = 0x06 QUIT = 0x07 STAT = 0x10 GETQ = 0x09 SETQ = 0x11 ADDQ = 0x12 DELETEQ = 0x14 NOOP = 0x0a =begin Possible values of the one-byte field: 0x00 Get 0x01 Set 0x02 Add 0x03 Replace 0x04 Delete 0x05 Increment 0x06 Decrement 0x07 Quit 0x08 Flush 0x09 GetQ 0x0A No-op 0x0B Version 0x0C GetK 0x0D GetKQ 0x0E Append 0x0F Prepend 0x10 Stat 0x11 SetQ 0x12 AddQ 0x13 ReplaceQ 0x14 DeleteQ 0x15 IncrementQ 0x16 DecrementQ 0x17 QuitQ 0x18 FlushQ 0x19 AppendQ 0x1A PrependQ =end end end ruby-remcached-0.4.1/lib/remcached/pack_array.rb000066400000000000000000000016441212266575700215520ustar00rootroot00000000000000## # Works exactly like Array#pack and String#unpack, except that it # inverts 'q' & 'Q' prior packing/after unpacking. This is done to # achieve network byte order for these values on a little-endian machine. # # FIXME: implement check for big-endian machines. module Memcached::PackArray def self.pack(ary, fmt1) fmt2 = '' values = [] fmt1.each_char do |c| if c == 'Q' || c == 'q' fmt2 += 'a8' values << [ary.shift].pack(c).reverse else fmt2 += c values << ary.shift end end values.pack(fmt2) end def self.unpack(buf, fmt1) fmt2 = '' reverse = [] i = 0 fmt1.each_char do |c| if c == 'Q' || c == 'q' fmt2 += 'a8' reverse << [i, c] else fmt2 += c end i += 1 end ary = buf.unpack(fmt2) reverse.each do |i, c| ary[i], = ary[i].reverse.unpack(c) end ary end end ruby-remcached-0.4.1/lib/remcached/packet.rb000066400000000000000000000177761212266575700207220ustar00rootroot00000000000000require 'remcached/pack_array' module Memcached class Packet ## # Initialize with fields def initialize(contents={}) @contents = contents (self.class.fields + self.class.extras).each do |name,fmt,default| self[name] ||= default if default end end ## # Get field def [](field) @contents[field] end ## # Set field def []=(field, value) @contents[field] = value end ## # Define a field for subclasses def self.field(name, packed, default=nil) instance_eval do @fields ||= [] @fields << [name, packed, default] end end ## # Fields of parent and this class def self.fields parent_class = ancestors[1] parent_fields = parent_class.respond_to?(:fields) ? parent_class.fields : [] class_fields = instance_eval { @fields || [] } parent_fields + class_fields end ## # Define an extra for subclasses def self.extra(name, packed, default=nil) instance_eval do @extras ||= [] @extras << [name, packed, default] end end ## # Extras of this class def self.extras parent_class = ancestors[1] parent_extras = parent_class.respond_to?(:extras) ? parent_class.extras : [] class_extras = instance_eval { @extras || [] } parent_extras + class_extras end ## # Build a packet by parsing header fields def self.parse_header(buf) pack_fmt = fields.collect { |name,fmt,default| fmt }.join values = PackArray.unpack(buf, pack_fmt) contents = {} fields.each do |name,fmt,default| contents[name] = values.shift end new contents end ## # Parse body of packet when the +:total_body_length+ field is # known by header. Pass it at least +total_body_length+ bytes. # # return:: [String] remaining bytes def parse_body(buf) if self[:total_body_length] < 1 buf, rest = "", buf else buf, rest = buf[0..(self[:total_body_length] - 1)], buf[self[:total_body_length]..-1] end if self[:extras_length] > 0 self[:extras] = parse_extras(buf[0..(self[:extras_length]-1)]) else self[:extras] = parse_extras("") end if self[:key_length] > 0 self[:key] = buf[self[:extras_length]..(self[:extras_length]+self[:key_length]-1)] else self[:key] = "" end self[:value] = buf[(self[:extras_length]+self[:key_length])..-1] rest end ## # Serialize for wire def to_s extras_s = extras_to_s key_s = self[:key].to_s value_s = self[:value].to_s self[:extras_length] = extras_s.length self[:key_length] = key_s.length self[:total_body_length] = extras_s.length + key_s.length + value_s.length header_to_s + extras_s + key_s + value_s end protected def parse_extras(buf) pack_fmt = self.class.extras.collect { |name,fmt,default| fmt }.join values = PackArray.unpack(buf, pack_fmt) self.class.extras.each do |name,fmt,default| @self[name] = values.shift || default end end def header_to_s pack_fmt = '' values = [] self.class.fields.each do |name,fmt,default| values << self[name] pack_fmt += fmt end PackArray.pack(values, pack_fmt) end def extras_to_s values = [] pack_fmt = '' self.class.extras.each do |name,fmt,default| values << self[name] || default pack_fmt += fmt end PackArray.pack(values, pack_fmt) end end ## # Request header: # # Byte/ 0 | 1 | 2 | 3 | # / | | | | # |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| # +---------------+---------------+---------------+---------------+ # 0| Magic | Opcode | Key length | # +---------------+---------------+---------------+---------------+ # 4| Extras length | Data type | Reserved | # +---------------+---------------+---------------+---------------+ # 8| Total body length | # +---------------+---------------+---------------+---------------+ # 12| Opaque | # +---------------+---------------+---------------+---------------+ # 16| CAS | # | | # +---------------+---------------+---------------+---------------+ # Total 24 bytes class Request < Packet field :magic, 'C', 0x80 field :opcode, 'C', 0 field :key_length, 'n' field :extras_length, 'C' field :data_type, 'C', 0 field :reserved, 'n', 0 field :total_body_length, 'N' field :opaque, 'N', 0 field :cas, 'Q', 0 def self.parse_header(buf) me = super me[:magic] == 0x80 ? me : nil end class Get < Request def initialize(contents) super({:opcode=>Commands::GET}.merge(contents)) end class Quiet < Get def initialize(contents) super({:opcode=>Commands::GETQ}.merge(contents)) end end end class Add < Request extra :flags, 'N', 0 extra :expiration, 'N', 0 def initialize(contents) super({:opcode=>Commands::ADD}.merge(contents)) end class Quiet < Add def initialize(contents) super({:opcode=>Commands::ADDQ}.merge(contents)) end end end class Set < Request extra :flags, 'N', 0 extra :expiration, 'N', 0 def initialize(contents) super({:opcode=>Commands::SET}.merge(contents)) end class Quiet < Set def initialize(contents) super({:opcode=>Commands::SETQ}.merge(contents)) end end end class Delete < Request def initialize(contents) super({:opcode=>Commands::DELETE}.merge(contents)) end class Quiet < Delete def initialize(contents) super({:opcode=>Commands::DELETEQ}.merge(contents)) end end end class Stats < Request def initialize(contents) super({:opcode=>Commands::STAT}.merge(contents)) end end class NoOp < Request def initialize super(:opcode=>Commands::NOOP) end end end ## # Response header: # # Byte/ 0 | 1 | 2 | 3 | # / | | | | # |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| # +---------------+---------------+---------------+---------------+ # 0| Magic | Opcode | Key Length | # +---------------+---------------+---------------+---------------+ # 4| Extras length | Data type | Status | # +---------------+---------------+---------------+---------------+ # 8| Total body length | # +---------------+---------------+---------------+---------------+ # 12| Opaque | # +---------------+---------------+---------------+---------------+ # 16| CAS | # | | # +---------------+---------------+---------------+---------------+ # Total 24 bytes class Response < Packet field :magic, 'C', 0x81 field :opcode, 'C', 0 field :key_length, 'n' field :extras_length, 'C' field :data_type, 'C', 0 field :status, 'n', Errors::NO_ERROR field :total_body_length, 'N' field :opaque, 'N', 0 field :cas, 'Q', 0 def self.parse_header(buf) me = super me[:magic] == 0x81 ? me : nil end end end ruby-remcached-0.4.1/metadata.yml000066400000000000000000000025531212266575700167330ustar00rootroot00000000000000--- !ruby/object:Gem::Specification name: remcached version: !ruby/object:Gem::Version version: 0.4.1 platform: ruby authors: - Stephan Maka autorequire: bindir: bin cert_chain: [] date: 2009-11-03 00:00:00 +01:00 default_executable: dependencies: [] description: Ruby EventMachine memcached client email: astro@spaceboyz.net executables: [] extensions: [] extra_rdoc_files: - README.rst files: - .gitignore - README.rst - Rakefile - VERSION.yml - examples/fill.rb - lib/remcached.rb - lib/remcached/client.rb - lib/remcached/const.rb - lib/remcached/pack_array.rb - lib/remcached/packet.rb - remcached.gemspec - spec/client_spec.rb - spec/memcached_spec.rb - spec/packet_spec.rb has_rdoc: true homepage: http://github.com/astro/remcached/ licenses: [] post_install_message: rdoc_options: - --charset=UTF-8 require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: "0" version: required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: "0" version: requirements: [] rubyforge_project: rubygems_version: 1.3.5 signing_key: specification_version: 3 summary: Ruby EventMachine memcached client test_files: - spec/memcached_spec.rb - spec/packet_spec.rb - spec/client_spec.rb - examples/fill.rb ruby-remcached-0.4.1/remcached.gemspec000066400000000000000000000027511212266575700177100ustar00rootroot00000000000000# Generated by jeweler # DO NOT EDIT THIS FILE # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec` # -*- encoding: utf-8 -*- Gem::Specification.new do |s| s.name = %q{remcached} s.version = "0.4.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Stephan Maka"] s.date = %q{2009-11-03} s.description = %q{Ruby EventMachine memcached client} s.email = %q{astro@spaceboyz.net} s.extra_rdoc_files = [ "README.rst" ] s.files = [ ".gitignore", "README.rst", "Rakefile", "VERSION.yml", "examples/fill.rb", "lib/remcached.rb", "lib/remcached/client.rb", "lib/remcached/const.rb", "lib/remcached/pack_array.rb", "lib/remcached/packet.rb", "remcached.gemspec", "spec/client_spec.rb", "spec/memcached_spec.rb", "spec/packet_spec.rb" ] s.homepage = %q{http://github.com/astro/remcached/} s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] s.rubygems_version = %q{1.3.5} s.summary = %q{Ruby EventMachine memcached client} s.test_files = [ "spec/memcached_spec.rb", "spec/packet_spec.rb", "spec/client_spec.rb", "examples/fill.rb" ] if s.respond_to? :specification_version then current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION s.specification_version = 3 if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then else end else end end ruby-remcached-0.4.1/spec/000077500000000000000000000000001212266575700153555ustar00rootroot00000000000000ruby-remcached-0.4.1/spec/client_spec.rb000066400000000000000000000015351212266575700201760ustar00rootroot00000000000000$: << File.dirname(__FILE__) + '/../lib' require 'remcached' describe Memcached::Client do def run(&block) EM.run do @cl = Memcached::Client.connect('localhost', &block) end end def stop @cl.close_connection EM.stop end context "when getting stats" do before :all do @stats = {} run do @cl.stats do |result| result[:status].should == Memcached::Errors::NO_ERROR if result[:key] != '' @stats[result[:key]] = result[:value] else stop end end end end it "should have received some keys" do @stats.should include(*%w(pid uptime time version curr_connections total_connections)) end end =begin it "should keep alive" do run do EM::Timer.new(30) do stop end end end =end end ruby-remcached-0.4.1/spec/memcached_spec.rb000066400000000000000000000157041212266575700206310ustar00rootroot00000000000000$: << File.dirname(__FILE__) + '/../lib' require 'remcached' describe Memcached do def run(&block) EM.run do Memcached.servers = %w(127.0.0.2 localhost:11212 localhost localhost) @timer = EM::PeriodicTimer.new(0.01) do # at least localhost & localhost if Memcached.usable_clients.length >= 2 @timer.cancel block.call end end end end def stop Memcached.servers = [] EM.stop end context "when doing a simple operation" do it "should add a value" do run do Memcached.add(:key => 'Hello', :value => 'World') do |result| result.should be_kind_of(Memcached::Response) result[:status].should == Memcached::Errors::NO_ERROR result[:cas].should_not == 0 stop end end end it "should get a value" do run do Memcached.get(:key => 'Hello') do |result| result.should be_kind_of(Memcached::Response) result[:status].should == Memcached::Errors::NO_ERROR result[:value].should == 'World' result[:cas].should_not == 0 @old_cas = result[:cas] stop end end end it "should set a value" do run do Memcached.set(:key => 'Hello', :value => 'Planet') do |result| result.should be_kind_of(Memcached::Response) result[:status].should == Memcached::Errors::NO_ERROR result[:cas].should_not == 0 result[:cas].should_not == @old_cas stop end end end it "should get a value" do run do Memcached.get(:key => 'Hello') do |result| result.should be_kind_of(Memcached::Response) result[:status].should == Memcached::Errors::NO_ERROR result[:value].should == 'Planet' result[:cas].should_not == @old_cas stop end end end it "should delete a value" do run do Memcached.delete(:key => 'Hello') do |result| result.should be_kind_of(Memcached::Response) result[:status].should == Memcached::Errors::NO_ERROR stop end end end it "should not get a value" do run do Memcached.get(:key => 'Hello') do |result| result.should be_kind_of(Memcached::Response) result[:status].should == Memcached::Errors::KEY_NOT_FOUND stop end end end $n = 100 context "when incrementing a counter #{$n} times" do it "should initialize the counter" do run do Memcached.set(:key => 'counter', :value => '0') do |result| stop end end end it "should count #{$n} times" do @counted = 0 def count Memcached.get(:key => 'counter') do |result| result[:status].should == Memcached::Errors::NO_ERROR value = result[:value].to_i Memcached.set(:key => 'counter', :value => (value + 1).to_s, :cas => result[:cas]) do |result| if result[:status] == Memcached::Errors::KEY_EXISTS count # again else result[:status].should == Memcached::Errors::NO_ERROR @counted += 1 stop if @counted >= $n end end end end run do $n.times { count } end end it "should have counted up to #{$n}" do run do Memcached.get(:key => 'counter') do |result| result[:status].should == Memcached::Errors::NO_ERROR result[:value].to_i.should == $n stop end end end end end context "when using multiple servers" do it "should not return the same hash for the succeeding key" do run do Memcached.hash_key('0').should_not == Memcached.hash_key('1') stop end end it "should not return the same client for the succeeding key" do run do # wait for 2nd client to be connected EM::Timer.new(0.1) do Memcached.client_for_key('0').should_not == Memcached.client_for_key('1') stop end end end it "should spread load (observe from outside :-)" do run do n = 10000 replies = 0 n.times do |i| Memcached.set(:key => "#{i % 100}", :value => rand(1 << 31).to_s) { replies += 1 stop if replies >= n } end end end end context "when manipulating multiple records at once" do before :all do @n = 10 end def key(n) "test:item:#{n}" end it "should add some items" do run do items = [] @n.times { |i| items << { :key => key(i), :value => 'Foo', :expiration => 20 } if i % 2 == 0 } Memcached.multi_add(items) { |responses| stop @n.times { |i| if i % 2 == 0 && (response_i = responses[key(i)]) response_i[:status].should == Memcached::Errors::NO_ERROR end } } end end it "should get all items" do run do items = [] @n.times { |i| items << { :key => key(i) } } Memcached.multi_get(items) { |responses| stop @n.times { |i| if i % 2 == 0 responses.should have_key(key(i)) responses[key(i)][:status].should == Memcached::Errors::NO_ERROR responses[key(i)][:value].should == 'Foo' else # either no response because request was quiet, or not # found in case of last response if (response_i = responses[key(i)]) response_i[:status].should == Memcached::Errors::KEY_NOT_FOUND end end } } end end it "should delete all items" do run do items = [] @n.times { |i| items << { :key => key(i) } } Memcached.multi_delete(items) { |responses| stop @n.times { |i| if i % 2 == 0 # either no response because request was quiet, or ok in # case of last response if (response_i = responses[key(i)]) response_i[:status].should == Memcached::Errors::NO_ERROR end else responses[key(i)][:status].should == Memcached::Errors::KEY_NOT_FOUND end } } end end context "when the multi operation is empty" do it "should return immediately" do @results = [] @calls = 0 Memcached.multi_add([]) { |responses| @results += responses @calls += 1 } @results.should be_empty @calls.should == 1 end end end end ruby-remcached-0.4.1/spec/packet_spec.rb000066400000000000000000000101031212266575700201560ustar00rootroot00000000000000$: << File.dirname(__FILE__) + '/../lib' require 'remcached' describe Memcached::Packet do context "when generating a request" do it "should set default values" do pkt = Memcached::Request.new pkt[:magic].should == 0x80 end context "example 4.2.1" do before :all do pkt = Memcached::Request.new(:key => 'Hello') @s = pkt.to_s end it "should serialize correctly" do @s.should == "\x80\x00\x00\x05" + "\x00\x00\x00\x00" + "\x00\x00\x00\x05" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "Hello" end end context "example 4.3.1 (add)" do before :all do pkt = Memcached::Request::Add.new(:flags => 0xdeadbeef, :expiration => 0xe10, :key => "Hello", :value => "World") @s = pkt.to_s end it "should serialize correctly" do @s.should == "\x80\x02\x00\x05" + "\x08\x00\x00\x00" + "\x00\x00\x00\x12" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\xde\xad\xbe\xef" + "\x00\x00\x0e\x10" + "Hello" + "World" end end end context "when parsing a response" do context "example 4.1.1" do before :all do s = "\x81\x00\x00\x00\x00\x00\x00\x01" + "\x00\x00\x00\x09\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00" + "Not found" @pkt = Memcached::Response.parse_header(s[0..23]) @pkt.parse_body(s[24..-1]) end it "should return the right class according to magic & opcode" do @pkt[:magic].should == 0x81 @pkt[:opcode].should == 0 @pkt.class.should == Memcached::Response end it "should return the right data type" do @pkt[:data_type].should == 0 end it "should return the right status" do @pkt[:status].should == Memcached::Errors::KEY_NOT_FOUND end it "should return the right opaque" do @pkt[:opaque].should == 0 end it "should return the right CAS" do @pkt[:cas].should == 0 end it "should parse the body correctly" do @pkt[:extras].should be_empty @pkt[:key].should == "" @pkt[:value].should == "Not found" end end context "example 4.2.1" do before :all do s = "\x81\x00\x00\x00" + "\x04\x00\x00\x00" + "\x00\x00\x00\x09" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00\x01" + "\xde\xad\xbe\xef" + "World" @pkt = Memcached::Response.parse_header(s[0..23]) @pkt.parse_body(s[24..-1]) end it "should return the right class according to magic & opcode" do @pkt[:magic].should == 0x81 @pkt[:opcode].should == 0 @pkt.class.should == Memcached::Response end it "should return the right data type" do @pkt[:data_type].should == 0 end it "should return the right status" do @pkt[:status].should == Memcached::Errors::NO_ERROR end it "should return the right opaque" do @pkt[:opaque].should == 0 end it "should return the right CAS" do @pkt[:cas].should == 1 end it "should parse the body correctly" do @pkt[:key].should == "" @pkt[:value].should == "World" end end describe :parse_body do it "should return succeeding bytes" do s = "\x81\x01\x00\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00\x01" + "chunky bacon" @pkt = Memcached::Response.parse_header(s[0..23]) s = @pkt.parse_body(s[24..-1]) @pkt[:status].should == 0 @pkt[:total_body_length].should == 0 @pkt[:value].should == "" s.should == "chunky bacon" end end end end