├── .gitignore ├── VERSION.yml ├── Rakefile ├── spec ├── client_spec.rb ├── packet_spec.rb └── memcached_spec.rb ├── lib ├── remcached │ ├── pack_array.rb │ ├── const.rb │ ├── client.rb │ └── packet.rb └── remcached.rb ├── remcached.gemspec ├── examples └── fill.rb ├── README.rst └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *# 2 | *~ 3 | -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :minor: 4 3 | :patch: 1 4 | :major: 0 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'jeweler' 3 | Jeweler::Tasks.new do |gemspec| 4 | gemspec.name = "remcached" 5 | gemspec.summary = "Ruby EventMachine memcached client" 6 | gemspec.description = gemspec.summary 7 | gemspec.email = "astro@spaceboyz.net" 8 | gemspec.homepage = "http://github.com/astro/remcached/" 9 | gemspec.authors = ["Stephan Maka"] 10 | end 11 | rescue LoadError 12 | puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 13 | end 14 | -------------------------------------------------------------------------------- /spec/client_spec.rb: -------------------------------------------------------------------------------- 1 | $: << File.dirname(__FILE__) + '/../lib' 2 | require 'remcached' 3 | 4 | describe Memcached::Client do 5 | 6 | def run(&block) 7 | EM.run do 8 | @cl = Memcached::Client.connect('localhost', &block) 9 | end 10 | end 11 | def stop 12 | @cl.close_connection 13 | EM.stop 14 | end 15 | 16 | 17 | context "when getting stats" do 18 | before :all do 19 | @stats = {} 20 | run do 21 | @cl.stats do |result| 22 | result[:status].should == Memcached::Errors::NO_ERROR 23 | if result[:key] != '' 24 | @stats[result[:key]] = result[:value] 25 | else 26 | stop 27 | end 28 | end 29 | end 30 | end 31 | 32 | it "should have received some keys" do 33 | @stats.should include(*%w(pid uptime time version curr_connections total_connections)) 34 | end 35 | end 36 | 37 | =begin 38 | it "should keep alive" do 39 | run do 40 | EM::Timer.new(30) do 41 | stop 42 | end 43 | end 44 | end 45 | =end 46 | end 47 | -------------------------------------------------------------------------------- /lib/remcached/pack_array.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Works exactly like Array#pack and String#unpack, except that it 3 | # inverts 'q' & 'Q' prior packing/after unpacking. This is done to 4 | # achieve network byte order for these values on a little-endian machine. 5 | # 6 | # FIXME: implement check for big-endian machines. 7 | module Memcached::PackArray 8 | def self.pack(ary, fmt1) 9 | fmt2 = '' 10 | values = [] 11 | fmt1.each_char do |c| 12 | if c == 'Q' || c == 'q' 13 | fmt2 += 'a8' 14 | values << [ary.shift].pack(c).reverse 15 | else 16 | fmt2 += c 17 | values << ary.shift 18 | end 19 | end 20 | 21 | values.pack(fmt2) 22 | end 23 | 24 | def self.unpack(buf, fmt1) 25 | fmt2 = '' 26 | reverse = [] 27 | i = 0 28 | fmt1.each_char do |c| 29 | if c == 'Q' || c == 'q' 30 | fmt2 += 'a8' 31 | reverse << [i, c] 32 | else 33 | fmt2 += c 34 | end 35 | i += 1 36 | end 37 | 38 | ary = buf.unpack(fmt2) 39 | 40 | reverse.each do |i, c| 41 | ary[i], = ary[i].reverse.unpack(c) 42 | end 43 | 44 | ary 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/remcached/const.rb: -------------------------------------------------------------------------------- 1 | module Memcached 2 | module Datatypes 3 | RAW_BYTES = 0x00 4 | end 5 | 6 | module Errors 7 | NO_ERROR = 0x0000 8 | KEY_NOT_FOUND = 0x0001 9 | KEY_EXISTS = 0x0002 10 | VALUE_TOO_LARGE = 0x0003 11 | INVALID_ARGS = 0x0004 12 | ITEM_NOT_STORED = 0x0005 13 | NON_NUMERIC_VALUE = 0x0006 14 | 15 | DISCONNECTED = 0xffff 16 | end 17 | 18 | module Commands 19 | GET = 0x00 20 | SET = 0x01 21 | ADD = 0x02 22 | REPLACE = 0x03 23 | DELETE = 0x04 24 | INCREMENT = 0x05 25 | DECREMENT = 0x06 26 | QUIT = 0x07 27 | STAT = 0x10 28 | GETQ = 0x09 29 | SETQ = 0x11 30 | ADDQ = 0x12 31 | DELETEQ = 0x14 32 | NOOP = 0x0a 33 | 34 | =begin 35 | Possible values of the one-byte field: 36 | 0x00 Get 37 | 0x01 Set 38 | 0x02 Add 39 | 0x03 Replace 40 | 0x04 Delete 41 | 0x05 Increment 42 | 0x06 Decrement 43 | 0x07 Quit 44 | 0x08 Flush 45 | 0x09 GetQ 46 | 0x0A No-op 47 | 0x0B Version 48 | 0x0C GetK 49 | 0x0D GetKQ 50 | 0x0E Append 51 | 0x0F Prepend 52 | 0x10 Stat 53 | 0x11 SetQ 54 | 0x12 AddQ 55 | 0x13 ReplaceQ 56 | 0x14 DeleteQ 57 | 0x15 IncrementQ 58 | 0x16 DecrementQ 59 | 0x17 QuitQ 60 | 0x18 FlushQ 61 | 0x19 AppendQ 62 | 0x1A PrependQ 63 | =end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /remcached.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec` 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{remcached} 8 | s.version = "0.4.1" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Stephan Maka"] 12 | s.date = %q{2009-11-03} 13 | s.description = %q{Ruby EventMachine memcached client} 14 | s.email = %q{astro@spaceboyz.net} 15 | s.extra_rdoc_files = [ 16 | "README.rst" 17 | ] 18 | s.files = [ 19 | ".gitignore", 20 | "README.rst", 21 | "Rakefile", 22 | "VERSION.yml", 23 | "examples/fill.rb", 24 | "lib/remcached.rb", 25 | "lib/remcached/client.rb", 26 | "lib/remcached/const.rb", 27 | "lib/remcached/pack_array.rb", 28 | "lib/remcached/packet.rb", 29 | "remcached.gemspec", 30 | "spec/client_spec.rb", 31 | "spec/memcached_spec.rb", 32 | "spec/packet_spec.rb" 33 | ] 34 | s.homepage = %q{http://github.com/astro/remcached/} 35 | s.rdoc_options = ["--charset=UTF-8"] 36 | s.require_paths = ["lib"] 37 | s.rubygems_version = %q{1.3.5} 38 | s.summary = %q{Ruby EventMachine memcached client} 39 | s.test_files = [ 40 | "spec/memcached_spec.rb", 41 | "spec/packet_spec.rb", 42 | "spec/client_spec.rb", 43 | "examples/fill.rb" 44 | ] 45 | 46 | if s.respond_to? :specification_version then 47 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 48 | s.specification_version = 3 49 | 50 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 51 | else 52 | end 53 | else 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /examples/fill.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Experimentally determine how many items fit in your memcached 4 | # instance. Adjust parameters below for your scenario. 5 | 6 | BATCH_SIZE = 10000 7 | KEY_SIZE = 26 8 | VALUE_SIZE = 0 9 | 10 | 11 | $: << File.dirname(__FILE__) + "/../lib" 12 | require 'remcached' 13 | 14 | EM.run do 15 | @total = 0 16 | 17 | # Action 18 | def fill 19 | old_total = @total 20 | 21 | reqs = (1..BATCH_SIZE).map { 22 | @total += 1 23 | { :key => sprintf("%0#{KEY_SIZE}X", @total), 24 | :value => sprintf("%0#{VALUE_SIZE}X", @total) 25 | } 26 | } 27 | Memcached.multi_add(reqs) do |resps| 28 | resps.each do |key,resp| 29 | case resp[:status] 30 | when Memcached::Errors::NO_ERROR 31 | :ok 32 | when Memcached::Errors::KEY_EXISTS 33 | @total -= 1 34 | else 35 | puts "Cannot set #{key}: status=#{resp[:status].inspect}" 36 | @total -= 1 37 | end 38 | end 39 | 40 | puts "Added #{@total - old_total}, now: #{@total}" 41 | if Memcached.usable? 42 | stats = {} 43 | Memcached.usable_clients[0].stats do |resp| 44 | if resp[:key] != '' 45 | stats[resp[:key]] = resp[:value] 46 | else 47 | puts "Stats: #{stats['bytes']} bytes in #{stats['curr_items']} of #{stats['total_items']} items" 48 | end 49 | end 50 | 51 | # Next round: 52 | fill 53 | else 54 | EM.stop 55 | end 56 | end 57 | end 58 | 59 | # Initialization & start 60 | Memcached.servers = %w(localhost) 61 | @t = EM::PeriodicTimer.new(0.01) do 62 | if Memcached.usable? 63 | puts "Connected to server" 64 | @t.cancel 65 | fill 66 | else 67 | puts "Waiting for server connection..." 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | remcached 2 | ========= 3 | 4 | * **Ruby EventMachine memCACHED client implementation** 5 | * provides a direct interface to the memcached protocol and its 6 | semantics 7 | * uses the memcached `binary protocol`_ to reduce parsing overhead on 8 | the server side (requires memcached >= 1.3) 9 | * supports multiple servers with simple round-robin key hashing 10 | (**TODO:** implement the libketama algorithm) in a fault-tolerant 11 | way 12 | * writing your own abstraction layer is recommended 13 | * uses RSpec 14 | * partially documented in RDoc-style 15 | 16 | 17 | Callbacks 18 | --------- 19 | 20 | Each request `may` be passed a callback. These are not two-cased 21 | (success & failure) EM deferrables, but standard Ruby callbacks. The 22 | rationale behind this is that there are no usual success/failure 23 | responses, but you will want to evaluate a ``response[:status]`` 24 | yourself to check for cache miss, version conflict, network 25 | disconnects etc. 26 | 27 | A callback may be kept if it returns ``:proceed`` to catch 28 | multi-response commands such as ``STAT``. 29 | 30 | remcached has been built with **fault tolerance** in mind: a callback 31 | will be called with just ``{:status => Memcached::Errors::DISCONNECTED}`` 32 | if the network connection has went away. Thus, you can expect your 33 | callback will be called, except of course you're using `quiet` 34 | commands. In that case, only a "non-usual response" from the server or 35 | a network failure will invoke your block. 36 | 37 | 38 | Multi commands 39 | -------------- 40 | 41 | The technique is described in the `binary protocol`_ spec in section 42 | **4.2**. ``Memcached.multi_operation`` will help you exactly with 43 | that, sending lots of those `quiet` commands, except for the last, 44 | which will be a `normal` command to trigger an acknowledge for all 45 | commands. 46 | 47 | This is of course implemented per-server to accomodate 48 | load-balancing. 49 | 50 | 51 | Usage 52 | ----- 53 | 54 | First, pass your memcached servers to the library:: 55 | 56 | Memcached.servers = %w(localhost localhost:11212 localhost:11213) 57 | 58 | Note that it won't be connected immediately. Use ``Memcached.usable?`` 59 | to check. This however complicates your own code and you can check 60 | ``response[:status] == Memcached::Errors::DISCONNECTED`` for network 61 | errors in all your response callbacks. 62 | 63 | Further usage is pretty straight-forward:: 64 | 65 | Memcached.get(:key => 'Hello') do |response| 66 | case response[:status] 67 | when Memcached::Errors::NO_ERROR 68 | use_cached_value response[:value] # ... 69 | when Memcached::Errors::KEY_NOT_FOUND 70 | refresh_cache! # ... 71 | when Memcached::Errors::DISCONNECTED 72 | proceed_uncached # ... 73 | else 74 | cry_for_help # ... 75 | end 76 | end 77 | end 78 | Memcached.set(:key => 'Hello', :value => 'World', 79 | :expiration => 600) do |response| 80 | case response[:status] 81 | when Memcached::Errors::NO_ERROR 82 | # That's good 83 | when Memcached::Errors::DISCONNECTED 84 | # Maybe stop filling the cache for now? 85 | else 86 | # What could've gone wrong? 87 | end 88 | end 89 | end 90 | 91 | Multi-commands may require a bit of precaution:: 92 | 93 | Memcached.multi_get([{:key => 'foo'}, 94 | {:key => 'bar'}]) do |responses| 95 | # responses is now a hash of Key => Response 96 | end 97 | 98 | It's not guaranteed that any of these keys will be present in the 99 | response. Moreover, they may be present even if they are a usual 100 | response because the last request is always non-quiet. 101 | 102 | 103 | **HAPPY CACHING!** 104 | 105 | .. _binary protocol: http://code.google.com/p/memcached/wiki/MemcacheBinaryProtocol 106 | -------------------------------------------------------------------------------- /lib/remcached.rb: -------------------------------------------------------------------------------- 1 | require 'remcached/const' 2 | require 'remcached/packet' 3 | require 'remcached/client' 4 | 5 | module Memcached 6 | class << self 7 | ## 8 | # +servers+: Array of host:port strings 9 | def servers=(servers) 10 | if defined?(@clients) && @clients 11 | while client = @clients.shift 12 | begin 13 | client.close 14 | rescue Exception 15 | # This is allowed to fail silently 16 | end 17 | end 18 | end 19 | 20 | @clients = servers.collect { |server| 21 | host, port = server.split(':') 22 | Client.connect host, (port ? port.to_i : 11211) 23 | } 24 | end 25 | 26 | def usable? 27 | usable_clients.length > 0 28 | end 29 | 30 | def usable_clients 31 | unless defined?(@clients) && @clients 32 | [] 33 | else 34 | @clients.select { |client| client.connected? } 35 | end 36 | end 37 | 38 | def client_for_key(key) 39 | usable_clients_ = usable_clients 40 | if usable_clients_.empty? 41 | nil 42 | else 43 | h = hash_key(key) % usable_clients_.length 44 | usable_clients_[h] 45 | end 46 | end 47 | 48 | def hash_key(key) 49 | hashed = 0 50 | i = 0 51 | key.each_byte do |b| 52 | j = key.length - i - 1 % 4 53 | hashed ^= b << (j * 8) 54 | i += 1 55 | end 56 | hashed 57 | end 58 | 59 | 60 | ## 61 | # Memcached operations 62 | ## 63 | 64 | def operation(request_klass, contents, &callback) 65 | client = client_for_key(contents[:key]) 66 | if client 67 | client.send_request request_klass.new(contents), &callback 68 | elsif callback 69 | callback.call :status => Errors::DISCONNECTED 70 | end 71 | end 72 | 73 | def add(contents, &callback) 74 | operation Request::Add, contents, &callback 75 | end 76 | def get(contents, &callback) 77 | operation Request::Get, contents, &callback 78 | end 79 | def set(contents, &callback) 80 | operation Request::Set, contents, &callback 81 | end 82 | def delete(contents, &callback) 83 | operation Request::Delete, contents, &callback 84 | end 85 | 86 | 87 | ## 88 | # Multi operations 89 | # 90 | ## 91 | 92 | def multi_operation(request_klass, contents_list, &callback) 93 | if contents_list.empty? 94 | callback.call [] 95 | return self 96 | end 97 | 98 | results = {} 99 | 100 | # Assemble client connections per keys 101 | client_contents = {} 102 | contents_list.each do |contents| 103 | client = client_for_key(contents[:key]) 104 | if client 105 | client_contents[client] ||= [] 106 | client_contents[client] << contents 107 | else 108 | puts "no client for #{contents[:key].inspect}" 109 | results[contents[:key]] = {:status => Memcached::Errors::DISCONNECTED} 110 | end 111 | end 112 | 113 | if client_contents.empty? 114 | callback.call results 115 | return self 116 | end 117 | 118 | # send requests and wait for responses per client 119 | clients_pending = client_contents.length 120 | client_contents.each do |client,contents_list| 121 | last_i = contents_list.length - 1 122 | client_results = {} 123 | 124 | contents_list.each_with_index do |contents,i| 125 | if i < last_i 126 | request = request_klass::Quiet.new(contents) 127 | client.send_request(request) { |response| 128 | results[contents[:key]] = response 129 | } 130 | else # last request for this client 131 | request = request_klass.new(contents) 132 | client.send_request(request) { |response| 133 | results[contents[:key]] = response 134 | clients_pending -= 1 135 | if clients_pending < 1 136 | callback.call results 137 | end 138 | } 139 | end 140 | end 141 | end 142 | 143 | self 144 | end 145 | 146 | def multi_add(contents_list, &callback) 147 | multi_operation Request::Add, contents_list, &callback 148 | end 149 | 150 | def multi_get(contents_list, &callback) 151 | multi_operation Request::Get, contents_list, &callback 152 | end 153 | 154 | def multi_set(contents_list, &callback) 155 | multi_operation Request::Set, contents_list, &callback 156 | end 157 | 158 | def multi_delete(contents_list, &callback) 159 | multi_operation Request::Delete, contents_list, &callback 160 | end 161 | 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/remcached/client.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | 3 | module Memcached 4 | class Connection < EventMachine::Connection 5 | def self.connect(host, port=11211, &connect_callback) 6 | df = EventMachine::DefaultDeferrable.new 7 | df.callback &connect_callback 8 | 9 | EventMachine.connect(host, port, self) do |me| 10 | me.instance_eval { 11 | @host, @port = host, port 12 | @connect_deferrable = df 13 | } 14 | end 15 | end 16 | 17 | def connected? 18 | @connected 19 | end 20 | 21 | def reconnect 22 | @connect_deferrable = EventMachine::DefaultDeferrable.new 23 | super @host, @port 24 | @connect_deferrable 25 | end 26 | 27 | def post_init 28 | @recv_buf = "" 29 | @recv_state = :header 30 | @connected = false 31 | @keepalive_timer = nil 32 | end 33 | 34 | def connection_completed 35 | @connected = true 36 | @connect_deferrable.succeed(self) 37 | 38 | @last_receive = Time.now 39 | @keepalive_timer = EventMachine::PeriodicTimer.new(1, &method(:keepalive)) 40 | end 41 | 42 | RECONNECT_DELAY = 10 43 | RECONNECT_JITTER = 5 44 | def unbind 45 | @keepalive_timer.cancel if @keepalive_timer 46 | 47 | @connected = false 48 | EventMachine::Timer.new(RECONNECT_DELAY + rand(RECONNECT_JITTER), 49 | method(:reconnect)) 50 | end 51 | 52 | RECEIVE_TIMEOUT = 15 53 | KEEPALIVE_INTERVAL = 5 54 | def keepalive 55 | if @last_receive + RECEIVE_TIMEOUT <= Time.now 56 | p :timeout 57 | close_connection 58 | elsif @last_receive + KEEPALIVE_INTERVAL <= Time.now 59 | send_keepalive 60 | end 61 | end 62 | 63 | def send_packet(pkt) 64 | send_data pkt.to_s 65 | end 66 | 67 | def receive_data(data) 68 | @recv_buf += data 69 | @last_receive = Time.now 70 | 71 | done = false 72 | while not done 73 | 74 | if @recv_state == :header && @recv_buf.length >= 24 75 | @received = Response.parse_header(@recv_buf[0..23]) 76 | @recv_buf = @recv_buf[24..-1] 77 | @recv_state = :body 78 | 79 | elsif @recv_state == :body && @recv_buf.length >= @received[:total_body_length] 80 | @recv_buf = @received.parse_body(@recv_buf) 81 | receive_packet(@received) 82 | 83 | @recv_state = :header 84 | 85 | else 86 | done = true 87 | end 88 | end 89 | end 90 | end 91 | 92 | class Client < Connection 93 | def post_init 94 | super 95 | @opaque_counter = 0 96 | @pending = [] 97 | end 98 | 99 | def unbind 100 | super 101 | @pending.each do |opaque, callback| 102 | callback.call :status => Errors::DISCONNECTED 103 | end 104 | @pending = [] 105 | end 106 | 107 | def send_request(pkt, &callback) 108 | @opaque_counter += 1 109 | @opaque_counter %= 1 << 32 110 | pkt[:opaque] = @opaque_counter 111 | send_packet pkt 112 | 113 | if callback 114 | @pending << [@opaque_counter, callback] 115 | end 116 | end 117 | 118 | ## 119 | # memcached responses possess the same order as their 120 | # corresponding requests. Therefore quiet requests that have not 121 | # yielded responses will be dropped silently to free memory from 122 | # +@pending+ 123 | # 124 | # When a callback has been fired and returned +:proceed+ without a 125 | # succeeding packet, we still keep it referenced around for 126 | # commands such as STAT which has multiple response packets. 127 | def receive_packet(response) 128 | pending_pos = nil 129 | pending_callback = nil 130 | @pending.each_with_index do |(pending_opaque,pending_cb),i| 131 | if response[:opaque] == pending_opaque 132 | pending_pos = i 133 | pending_callback = pending_cb 134 | break 135 | end 136 | end 137 | 138 | if pending_pos 139 | @pending = @pending[pending_pos..-1] 140 | begin 141 | if pending_callback.call(response) != :proceed 142 | @pending.shift 143 | end 144 | rescue Exception => e 145 | $stderr.puts "#{e.class}: #{e}\n" + e.backtrace.join("\n") 146 | end 147 | end 148 | end 149 | 150 | def send_keepalive 151 | send_request Request::NoOp.new 152 | end 153 | 154 | # Callback will be called multiple times 155 | def stats(contents={}, &callback) 156 | send_request Request::Stats.new(contents) do |result| 157 | callback.call result 158 | 159 | if result[:status] == Errors::NO_ERROR && result[:key] != '' 160 | :proceed 161 | end 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/packet_spec.rb: -------------------------------------------------------------------------------- 1 | $: << File.dirname(__FILE__) + '/../lib' 2 | require 'remcached' 3 | 4 | describe Memcached::Packet do 5 | 6 | context "when generating a request" do 7 | it "should set default values" do 8 | pkt = Memcached::Request.new 9 | pkt[:magic].should == 0x80 10 | end 11 | 12 | context "example 4.2.1" do 13 | before :all do 14 | pkt = Memcached::Request.new(:key => 'Hello') 15 | @s = pkt.to_s 16 | end 17 | 18 | it "should serialize correctly" do 19 | @s.should == ("\x80\x00\x00\x05" + 20 | "\x00\x00\x00\x00" + 21 | "\x00\x00\x00\x05" + 22 | "\x00\x00\x00\x00" + 23 | "\x00\x00\x00\x00" + 24 | "\x00\x00\x00\x00" + 25 | "Hello").force_encoding("ASCII-8BIT") 26 | end 27 | end 28 | 29 | context "example 4.3.1 (add)" do 30 | before :all do 31 | pkt = Memcached::Request::Add.new(:flags => 0xdeadbeef, 32 | :expiration => 0xe10, 33 | :key => "Hello", 34 | :value => "World") 35 | @s = pkt.to_s 36 | end 37 | 38 | it "should serialize correctly" do 39 | @s.should == ("\x80\x02\x00\x05" + 40 | "\x08\x00\x00\x00" + 41 | "\x00\x00\x00\x12" + 42 | "\x00\x00\x00\x00" + 43 | "\x00\x00\x00\x00" + 44 | "\x00\x00\x00\x00" + 45 | "\xde\xad\xbe\xef" + 46 | "\x00\x00\x0e\x10" + 47 | "Hello" + 48 | "World").force_encoding("ASCII-8BIT") 49 | end 50 | end 51 | end 52 | 53 | context "when parsing a response" do 54 | context "example 4.1.1" do 55 | before :all do 56 | s = ("\x81\x00\x00\x00\x00\x00\x00\x01" + 57 | "\x00\x00\x00\x09\x00\x00\x00\x00" + 58 | "\x00\x00\x00\x00\x00\x00\x00\x00" + 59 | "Not found").force_encoding("ASCII-8BIT") 60 | @pkt = Memcached::Response.parse_header(s[0..23]) 61 | @pkt.parse_body(s[24..-1]) 62 | end 63 | 64 | it "should return the right class according to magic & opcode" do 65 | @pkt[:magic].should == 0x81 66 | @pkt[:opcode].should == 0 67 | @pkt.class.should == Memcached::Response 68 | end 69 | it "should return the right data type" do 70 | @pkt[:data_type].should == 0 71 | end 72 | it "should return the right status" do 73 | @pkt[:status].should == Memcached::Errors::KEY_NOT_FOUND 74 | end 75 | it "should return the right opaque" do 76 | @pkt[:opaque].should == 0 77 | end 78 | it "should return the right CAS" do 79 | @pkt[:cas].should == 0 80 | end 81 | it "should parse the body correctly" do 82 | @pkt[:extras].should be_empty 83 | @pkt[:key].should == "" 84 | @pkt[:value].should == "Not found" 85 | end 86 | end 87 | 88 | context "example 4.2.1" do 89 | before :all do 90 | s = ("\x81\x00\x00\x00" + 91 | "\x04\x00\x00\x00" + 92 | "\x00\x00\x00\x09" + 93 | "\x00\x00\x00\x00" + 94 | "\x00\x00\x00\x00" + 95 | "\x00\x00\x00\x01" + 96 | "\xde\xad\xbe\xef" + 97 | "World").force_encoding("ASCII-8BIT") 98 | @pkt = Memcached::Response.parse_header(s[0..23]) 99 | @pkt.parse_body(s[24..-1]) 100 | end 101 | 102 | it "should return the right class according to magic & opcode" do 103 | @pkt[:magic].should == 0x81 104 | @pkt[:opcode].should == 0 105 | @pkt.class.should == Memcached::Response 106 | end 107 | it "should return the right data type" do 108 | @pkt[:data_type].should == 0 109 | end 110 | it "should return the right status" do 111 | @pkt[:status].should == Memcached::Errors::NO_ERROR 112 | end 113 | it "should return the right opaque" do 114 | @pkt[:opaque].should == 0 115 | end 116 | it "should return the right CAS" do 117 | @pkt[:cas].should == 1 118 | end 119 | it "should parse the body correctly" do 120 | @pkt[:key].should == "" 121 | @pkt[:value].should == "World" 122 | end 123 | end 124 | 125 | describe :parse_body do 126 | it "should return succeeding bytes" do 127 | s = "\x81\x01\x00\x00" + 128 | "\x00\x00\x00\x00" + 129 | "\x00\x00\x00\x00" + 130 | "\x00\x00\x00\x00" + 131 | "\x00\x00\x00\x00" + 132 | "\x00\x00\x00\x01" + 133 | "chunky bacon" 134 | @pkt = Memcached::Response.parse_header(s[0..23]) 135 | s = @pkt.parse_body(s[24..-1]) 136 | @pkt[:status].should == 0 137 | @pkt[:total_body_length].should == 0 138 | @pkt[:value].should == "" 139 | s.should == "chunky bacon" 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /spec/memcached_spec.rb: -------------------------------------------------------------------------------- 1 | $: << File.dirname(__FILE__) + '/../lib' 2 | require 'remcached' 3 | require 'em-spec/rspec' 4 | 5 | describe Memcached do 6 | def run(&block) 7 | EM.run do 8 | Memcached.servers = %w(127.0.0.2 localhost:11212 localhost localhost) 9 | 10 | @timer = EM::PeriodicTimer.new(0.01) do 11 | # at least localhost & localhost 12 | if Memcached.usable_clients.length >= 2 13 | @timer.cancel 14 | block.call 15 | end 16 | end 17 | 18 | end 19 | end 20 | def stop 21 | Memcached.servers = [] 22 | EM.stop 23 | end 24 | 25 | 26 | context "when doing a simple operation" do 27 | it "should add a value" do 28 | run do 29 | Memcached.add(:key => 'Hello', 30 | :value => 'World') do |result| 31 | result.should be_kind_of(Memcached::Response) 32 | result[:status].should == Memcached::Errors::NO_ERROR 33 | result[:cas].should_not == 0 34 | stop 35 | end 36 | end 37 | end 38 | 39 | it "should get a value" do 40 | run do 41 | Memcached.get(:key => 'Hello') do |result| 42 | result.should be_kind_of(Memcached::Response) 43 | result[:status].should == Memcached::Errors::NO_ERROR 44 | result[:value].should == 'World' 45 | result[:cas].should_not == 0 46 | @old_cas = result[:cas] 47 | stop 48 | end 49 | end 50 | end 51 | 52 | it "should set a value" do 53 | run do 54 | Memcached.set(:key => 'Hello', 55 | :value => 'Planet') do |result| 56 | result.should be_kind_of(Memcached::Response) 57 | result[:status].should == Memcached::Errors::NO_ERROR 58 | result[:cas].should_not == 0 59 | result[:cas].should_not == @old_cas 60 | stop 61 | end 62 | end 63 | end 64 | 65 | it "should get a value" do 66 | run do 67 | Memcached.get(:key => 'Hello') do |result| 68 | result.should be_kind_of(Memcached::Response) 69 | result[:status].should == Memcached::Errors::NO_ERROR 70 | result[:value].should == 'Planet' 71 | result[:cas].should_not == @old_cas 72 | stop 73 | end 74 | end 75 | end 76 | 77 | it "should delete a value" do 78 | run do 79 | Memcached.delete(:key => 'Hello') do |result| 80 | result.should be_kind_of(Memcached::Response) 81 | result[:status].should == Memcached::Errors::NO_ERROR 82 | stop 83 | end 84 | end 85 | end 86 | 87 | it "should not get a value" do 88 | run do 89 | Memcached.get(:key => 'Hello') do |result| 90 | result.should be_kind_of(Memcached::Response) 91 | result[:status].should == Memcached::Errors::KEY_NOT_FOUND 92 | stop 93 | end 94 | end 95 | end 96 | 97 | $n = 100 98 | context "when incrementing a counter #{$n} times" do 99 | it "should initialize the counter" do 100 | run do 101 | Memcached.set(:key => 'counter', 102 | :value => '0') do |result| 103 | stop 104 | end 105 | end 106 | end 107 | 108 | it "should count #{$n} times" do 109 | @counted = 0 110 | def count 111 | Memcached.get(:key => 'counter') do |result| 112 | result[:status].should == Memcached::Errors::NO_ERROR 113 | value = result[:value].to_i 114 | Memcached.set(:key => 'counter', 115 | :value => (value + 1).to_s, 116 | :cas => result[:cas]) do |result| 117 | if result[:status] == Memcached::Errors::KEY_EXISTS 118 | count # again 119 | else 120 | result[:status].should == Memcached::Errors::NO_ERROR 121 | @counted += 1 122 | stop if @counted >= $n 123 | end 124 | end 125 | end 126 | end 127 | run do 128 | $n.times { count } 129 | end 130 | end 131 | 132 | it "should have counted up to #{$n}" do 133 | run do 134 | Memcached.get(:key => 'counter') do |result| 135 | result[:status].should == Memcached::Errors::NO_ERROR 136 | result[:value].to_i.should == $n 137 | stop 138 | end 139 | end 140 | end 141 | end 142 | end 143 | 144 | context "when using multiple servers" do 145 | it "should not return the same hash for the succeeding key" do 146 | run do 147 | Memcached.hash_key('0').should_not == Memcached.hash_key('1') 148 | stop 149 | end 150 | end 151 | 152 | it "should not return the same client for the succeeding key" do 153 | run do 154 | # wait for 2nd client to be connected 155 | EM::Timer.new(0.1) do 156 | Memcached.client_for_key('0').should_not == Memcached.client_for_key('1') 157 | stop 158 | end 159 | end 160 | end 161 | 162 | it "should spread load (observe from outside :-)" do 163 | run do 164 | 165 | n = 10000 166 | replies = 0 167 | n.times do |i| 168 | Memcached.set(:key => "#{i % 100}", 169 | :value => rand(1 << 31).to_s) { 170 | replies += 1 171 | stop if replies >= n 172 | } 173 | end 174 | end 175 | 176 | end 177 | end 178 | 179 | context "when manipulating multiple records at once" do 180 | before :all do 181 | @n = 10 182 | end 183 | 184 | def key(n) 185 | "test:item:#{n}" 186 | end 187 | 188 | it "should add some items" do 189 | run do 190 | items = [] 191 | @n.times { |i| 192 | items << { :key => key(i), 193 | :value => 'Foo', 194 | :expiration => 20 } if i % 2 == 0 195 | } 196 | Memcached.multi_add(items) { |responses| 197 | stop 198 | @n.times { |i| 199 | if i % 2 == 0 && (response_i = responses[key(i)]) 200 | response_i[:status].should == Memcached::Errors::NO_ERROR 201 | end 202 | } 203 | } 204 | end 205 | end 206 | 207 | it "should get all items" do 208 | run do 209 | items = [] 210 | @n.times { |i| 211 | items << { :key => key(i) } 212 | } 213 | Memcached.multi_get(items) { |responses| 214 | stop 215 | @n.times { |i| 216 | if i % 2 == 0 217 | responses.should have_key(key(i)) 218 | responses[key(i)][:status].should == Memcached::Errors::NO_ERROR 219 | responses[key(i)][:value].should == 'Foo' 220 | else 221 | # either no response because request was quiet, or not 222 | # found in case of last response 223 | if (response_i = responses[key(i)]) 224 | response_i[:status].should == Memcached::Errors::KEY_NOT_FOUND 225 | end 226 | end 227 | } 228 | } 229 | end 230 | end 231 | 232 | it "should delete all items" do 233 | run do 234 | items = [] 235 | @n.times { |i| 236 | items << { :key => key(i) } 237 | } 238 | Memcached.multi_delete(items) { |responses| 239 | stop 240 | @n.times { |i| 241 | if i % 2 == 0 242 | # either no response because request was quiet, or ok in 243 | # case of last response 244 | if (response_i = responses[key(i)]) 245 | response_i[:status].should == Memcached::Errors::NO_ERROR 246 | end 247 | else 248 | responses[key(i)][:status].should == Memcached::Errors::KEY_NOT_FOUND 249 | end 250 | } 251 | } 252 | end 253 | end 254 | 255 | context "when the multi operation is empty" do 256 | it "should return immediately" do 257 | @results = [] 258 | @calls = 0 259 | Memcached.multi_add([]) { |responses| 260 | @results += responses 261 | @calls += 1 262 | } 263 | @results.should be_empty 264 | @calls.should == 1 265 | end 266 | end 267 | 268 | context "when servers are disconnected" do 269 | include EM::SpecHelper 270 | 271 | it "should return immediately with response" do 272 | em(1) do 273 | keys = [ { :key => key(0) } ] 274 | Memcached.multi_get(keys) { |responses| 275 | responses[key(0)][:status].should == Memcached::Errors::DISCONNECTED 276 | stop 277 | } 278 | end 279 | end 280 | end 281 | end 282 | 283 | end 284 | -------------------------------------------------------------------------------- /lib/remcached/packet.rb: -------------------------------------------------------------------------------- 1 | require 'remcached/pack_array' 2 | 3 | module Memcached 4 | class Packet 5 | ## 6 | # Initialize with fields 7 | def initialize(contents={}) 8 | @contents = contents 9 | (self.class.fields + 10 | self.class.extras).each do |name,fmt,default| 11 | self[name] ||= default if default 12 | end 13 | end 14 | 15 | ## 16 | # Get field 17 | def [](field) 18 | @contents[field] 19 | end 20 | 21 | ## 22 | # Set field 23 | def []=(field, value) 24 | @contents[field] = value 25 | end 26 | 27 | ## 28 | # Define a field for subclasses 29 | def self.field(name, packed, default=nil) 30 | instance_eval do 31 | @fields ||= [] 32 | @fields << [name, packed, default] 33 | end 34 | end 35 | 36 | ## 37 | # Fields of parent and this class 38 | def self.fields 39 | parent_class = ancestors[1] 40 | parent_fields = parent_class.respond_to?(:fields) ? parent_class.fields : [] 41 | class_fields = instance_eval { @fields || [] } 42 | parent_fields + class_fields 43 | end 44 | 45 | ## 46 | # Define an extra for subclasses 47 | def self.extra(name, packed, default=nil) 48 | instance_eval do 49 | @extras ||= [] 50 | @extras << [name, packed, default] 51 | end 52 | end 53 | 54 | ## 55 | # Extras of this class 56 | def self.extras 57 | parent_class = ancestors[1] 58 | parent_extras = parent_class.respond_to?(:extras) ? parent_class.extras : [] 59 | class_extras = instance_eval { @extras || [] } 60 | parent_extras + class_extras 61 | end 62 | 63 | ## 64 | # Build a packet by parsing header fields 65 | def self.parse_header(buf) 66 | pack_fmt = fields.collect { |name,fmt,default| fmt }.join 67 | values = PackArray.unpack(buf, pack_fmt) 68 | 69 | contents = {} 70 | fields.each do |name,fmt,default| 71 | contents[name] = values.shift 72 | end 73 | 74 | new contents 75 | end 76 | 77 | ## 78 | # Parse body of packet when the +:total_body_length+ field is 79 | # known by header. Pass it at least +total_body_length+ bytes. 80 | # 81 | # return:: [String] remaining bytes 82 | def parse_body(buf) 83 | if self[:total_body_length] < 1 84 | buf, rest = "", buf 85 | else 86 | buf, rest = buf[0..(self[:total_body_length] - 1)], buf[self[:total_body_length]..-1] 87 | end 88 | 89 | if self[:extras_length] > 0 90 | self[:extras] = parse_extras(buf[0..(self[:extras_length]-1)]) 91 | else 92 | self[:extras] = parse_extras("") 93 | end 94 | if self[:key_length] > 0 95 | self[:key] = buf[self[:extras_length]..(self[:extras_length]+self[:key_length]-1)] 96 | else 97 | self[:key] = "" 98 | end 99 | self[:value] = buf[(self[:extras_length]+self[:key_length])..-1] 100 | 101 | rest 102 | end 103 | 104 | ## 105 | # Serialize for wire 106 | def to_s 107 | extras_s = extras_to_s 108 | key_s = self[:key].to_s 109 | value_s = self[:value].to_s 110 | self[:extras_length] = extras_s.length 111 | self[:key_length] = key_s.length 112 | self[:total_body_length] = extras_s.length + key_s.length + value_s.length 113 | header_to_s + extras_s + key_s + value_s 114 | end 115 | 116 | protected 117 | 118 | def parse_extras(buf) 119 | pack_fmt = self.class.extras.collect { |name,fmt,default| fmt }.join 120 | values = PackArray.unpack(buf, pack_fmt) 121 | self.class.extras.each do |name,fmt,default| 122 | @self[name] = values.shift || default 123 | end 124 | end 125 | 126 | def header_to_s 127 | pack_fmt = '' 128 | values = [] 129 | self.class.fields.each do |name,fmt,default| 130 | values << self[name] 131 | pack_fmt += fmt 132 | end 133 | PackArray.pack(values, pack_fmt) 134 | end 135 | 136 | def extras_to_s 137 | values = [] 138 | pack_fmt = '' 139 | self.class.extras.each do |name,fmt,default| 140 | values << self[name] || default 141 | pack_fmt += fmt 142 | end 143 | 144 | PackArray.pack(values, pack_fmt) 145 | end 146 | end 147 | 148 | ## 149 | # Request header: 150 | # 151 | # Byte/ 0 | 1 | 2 | 3 | 152 | # / | | | | 153 | # |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| 154 | # +---------------+---------------+---------------+---------------+ 155 | # 0| Magic | Opcode | Key length | 156 | # +---------------+---------------+---------------+---------------+ 157 | # 4| Extras length | Data type | Reserved | 158 | # +---------------+---------------+---------------+---------------+ 159 | # 8| Total body length | 160 | # +---------------+---------------+---------------+---------------+ 161 | # 12| Opaque | 162 | # +---------------+---------------+---------------+---------------+ 163 | # 16| CAS | 164 | # | | 165 | # +---------------+---------------+---------------+---------------+ 166 | # Total 24 bytes 167 | class Request < Packet 168 | field :magic, 'C', 0x80 169 | field :opcode, 'C', 0 170 | field :key_length, 'n' 171 | field :extras_length, 'C' 172 | field :data_type, 'C', 0 173 | field :reserved, 'n', 0 174 | field :total_body_length, 'N' 175 | field :opaque, 'N', 0 176 | field :cas, 'Q', 0 177 | 178 | def self.parse_header(buf) 179 | me = super 180 | me[:magic] == 0x80 ? me : nil 181 | end 182 | 183 | class Get < Request 184 | def initialize(contents) 185 | super({:opcode=>Commands::GET}.merge(contents)) 186 | end 187 | 188 | class Quiet < Get 189 | def initialize(contents) 190 | super({:opcode=>Commands::GETQ}.merge(contents)) 191 | end 192 | end 193 | end 194 | 195 | class Add < Request 196 | extra :flags, 'N', 0 197 | extra :expiration, 'N', 0 198 | 199 | def initialize(contents) 200 | super({:opcode=>Commands::ADD}.merge(contents)) 201 | end 202 | 203 | class Quiet < Add 204 | def initialize(contents) 205 | super({:opcode=>Commands::ADDQ}.merge(contents)) 206 | end 207 | end 208 | end 209 | 210 | class Set < Request 211 | extra :flags, 'N', 0 212 | extra :expiration, 'N', 0 213 | 214 | def initialize(contents) 215 | super({:opcode=>Commands::SET}.merge(contents)) 216 | end 217 | 218 | class Quiet < Set 219 | def initialize(contents) 220 | super({:opcode=>Commands::SETQ}.merge(contents)) 221 | end 222 | end 223 | end 224 | 225 | class Delete < Request 226 | def initialize(contents) 227 | super({:opcode=>Commands::DELETE}.merge(contents)) 228 | end 229 | 230 | class Quiet < Delete 231 | def initialize(contents) 232 | super({:opcode=>Commands::DELETEQ}.merge(contents)) 233 | end 234 | end 235 | end 236 | 237 | class Stats < Request 238 | def initialize(contents) 239 | super({:opcode=>Commands::STAT}.merge(contents)) 240 | end 241 | end 242 | 243 | class NoOp < Request 244 | def initialize 245 | super(:opcode=>Commands::NOOP) 246 | end 247 | end 248 | end 249 | 250 | ## 251 | # Response header: 252 | # 253 | # Byte/ 0 | 1 | 2 | 3 | 254 | # / | | | | 255 | # |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| 256 | # +---------------+---------------+---------------+---------------+ 257 | # 0| Magic | Opcode | Key Length | 258 | # +---------------+---------------+---------------+---------------+ 259 | # 4| Extras length | Data type | Status | 260 | # +---------------+---------------+---------------+---------------+ 261 | # 8| Total body length | 262 | # +---------------+---------------+---------------+---------------+ 263 | # 12| Opaque | 264 | # +---------------+---------------+---------------+---------------+ 265 | # 16| CAS | 266 | # | | 267 | # +---------------+---------------+---------------+---------------+ 268 | # Total 24 bytes 269 | class Response < Packet 270 | field :magic, 'C', 0x81 271 | field :opcode, 'C', 0 272 | field :key_length, 'n' 273 | field :extras_length, 'C' 274 | field :data_type, 'C', 0 275 | field :status, 'n', Errors::NO_ERROR 276 | field :total_body_length, 'N' 277 | field :opaque, 'N', 0 278 | field :cas, 'Q', 0 279 | 280 | def self.parse_header(buf) 281 | me = super 282 | me[:magic] == 0x81 ? me : nil 283 | end 284 | end 285 | end 286 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------