├── .gitignore ├── .travis.yml ├── LICENSE-MIT ├── Makefile ├── README.md └── src ├── bench └── main.rs ├── examples ├── server │ └── main.rs └── simple │ └── main.rs └── redis └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | .rust 2 | bin 3 | lib 4 | build 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - yes | sudo add-apt-repository ppa:hansjorg/rust 3 | - sudo apt-get update 4 | install: 5 | - sudo apt-get install rust-nightly 6 | script: 7 | - make compile 8 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Michael Neumann 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | compile: 2 | mkdir -p lib bin 3 | rustc --out-dir lib src/redis/lib.rs 4 | rustc -L lib -o bin/simple src/examples/simple/main.rs 5 | rustc -L lib -o bin/server src/examples/server/main.rs 6 | rustc -O -L lib -o bin/bench src/bench/main.rs 7 | clean: 8 | rm -rf lib bin 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-redis [![Build Status][travis-image]][travis-link] 2 | 3 | [travis-image]: https://travis-ci.org/mneumann/rust-redis.png?branch=master 4 | [travis-link]: https://travis-ci.org/mneumann/rust-redis 5 | 6 | A [Rust][rust-home] client/server library for [Redis][redis-home]. 7 | 8 | [rust-home]: http://www.rust-lang.org 9 | [redis-home]: http://redis.io 10 | 11 | ## Quickstart 12 | 13 | ```rust 14 | extern mod redis = "redis#0.1"; 15 | 16 | fn main() { 17 | let mut redis = redis::Client::new("127.0.0.1:6379"); 18 | 19 | redis.set_int("counter", 1); 20 | redis.set("key", "Hello"); 21 | 22 | redis.incr("counter"); 23 | 24 | let counter = redis.get_int("counter").unwrap(); 25 | println!("counter = {}", counter); 26 | 27 | let key = redis.get_str("key").unwrap(); 28 | println!("key = {}", key); 29 | 30 | match redis.get("key") { 31 | redis::Nil => { 32 | println!("Key not found") 33 | } 34 | redis::Data(ref s) => { 35 | println!("{:?}", std::str::from_utf8(*s)) 36 | } 37 | _ => { fail!() } 38 | } 39 | } 40 | ``` 41 | 42 | For a simple redis server implementation which supports GET/SET commands see 43 | examples/server/main.rs. 44 | 45 | ## Performance 46 | 47 | I did some early performance benchmarks with rust-0.9 and compared it against 48 | redis-benchmark. 49 | 50 | redis-benchmark -n 100000 -c 1 -t set # ~18500 reqs/sec 51 | redis-benchmark -n 100000 -c 2 -t set # ~27000 reqs/sec 52 | redis-benchmark -n 100000 -c 4 -t set # ~36000 reqs/sec 53 | redis-benchmark -n 100000 -c 8 -t set # ~44000 reqs/sec 54 | 55 | ./bin/bench 1 100000 # ~19500 reqs/sec 56 | ./bin/bench 2 100000 # ~19100 reqs/sec 57 | ./bin/bench 4 100000 # ~18800 reqs/sec 58 | ./bin/bench 8 100000 # ~18000 reqs/sec 59 | 60 | At this simple benchmark, rust-redis consistently shows about 18000 requests 61 | per second regardless of concurrency. I think this is because the way 62 | scheduling works. Using native threads would probably lead to the same 63 | performance as redis-benchmark. 64 | 65 | ## License 66 | 67 | rust-redis is under the MIT license, see LICENSE-MIT for details. 68 | -------------------------------------------------------------------------------- /src/bench/main.rs: -------------------------------------------------------------------------------- 1 | extern crate redis = "redis#0.1"; 2 | extern crate time; 3 | extern crate native; 4 | 5 | use redis::Client; 6 | use native::task; 7 | use std::comm::channel; 8 | 9 | fn bench_set(tid: uint, n: uint) { 10 | let mut redis = Client::new("127.0.0.1:6379"); 11 | 12 | for _ in range(0, n) { 13 | redis.set("key", "12"); 14 | } 15 | 16 | println!("Thread {} finished", tid); 17 | } 18 | 19 | fn main() { 20 | let before = time::precise_time_ns(); 21 | 22 | let concurrency: uint = from_str(std::os::args()[1]).unwrap(); 23 | let repeats: uint = from_str(std::os::args()[2]).unwrap(); 24 | let per_thread: uint = repeats / concurrency; 25 | let total_reqs = per_thread * concurrency; 26 | 27 | let mut threads = ~[]; 28 | 29 | for tid in range(0, concurrency) { 30 | println!("Thread {} started", tid); 31 | 32 | let (sender, receiver) = channel(); 33 | task::spawn(proc() { 34 | bench_set(tid, per_thread); 35 | sender.send(()); 36 | }); 37 | threads.push(receiver); 38 | } 39 | 40 | println!("Waiting for all clients to terminate"); 41 | for receiver in threads.iter() { 42 | receiver.recv(); 43 | } 44 | 45 | let after = time::precise_time_ns(); 46 | 47 | let time = ((after - before) / 1_000_000) as f64 / 1000f64; 48 | 49 | println!("Concurrency: {}", concurrency); 50 | println!("Total requests: {}", total_reqs); 51 | println!("Total time: {}", time); 52 | let reqs_per_s = total_reqs as f64 / time; 53 | println!("Requests per second: {}", reqs_per_s); 54 | } 55 | -------------------------------------------------------------------------------- /src/examples/server/main.rs: -------------------------------------------------------------------------------- 1 | #[feature(phase)]; 2 | 3 | /* 4 | * An example redis server accepting GET and SET requests 5 | * listening on 127.0.0.1:8000. 6 | * 7 | * You can test it with "redis-cli -p 8000". 8 | * 9 | * Copyright (c) 2014 by Michael Neumann 10 | * 11 | */ 12 | 13 | extern crate redis = "redis#0.1"; 14 | extern crate sync = "sync#0.10-pre"; 15 | extern crate collections; 16 | #[phase(syntax, link)] extern crate log; 17 | 18 | use std::io::net::ip::SocketAddr; 19 | use std::io::net::tcp::{TcpListener,TcpStream}; 20 | use std::io::{Listener,Acceptor,Writer}; 21 | use std::io::BufferedStream; 22 | use std::task; 23 | use collections::hashmap::HashMap; 24 | use sync::RWArc; 25 | 26 | fn handle_connection(conn: TcpStream, shared_ht: RWArc>) { 27 | debug!("Got connection"); 28 | 29 | let mut io = BufferedStream::new(conn); 30 | 31 | loop { 32 | match redis::parse(&mut io).unwrap() { 33 | redis::List(ref lst) => { 34 | match lst.get(0) { 35 | Some(&redis::Data(ref command)) => { 36 | if command.as_slice() == bytes!("GET") { 37 | match (lst.len(), lst.get(1)) { 38 | (2, Some(&redis::Data(ref key))) => { 39 | debug!("GET: {:s}", std::str::from_utf8(key.as_slice()).unwrap()); 40 | let mut cwr = redis::CommandWriter::new(); 41 | shared_ht.read(|ht| { 42 | match ht.find(key) { 43 | Some(val) => { 44 | cwr.args(1); 45 | cwr.arg_bin(*val); 46 | } 47 | None => { 48 | cwr.nil(); 49 | } 50 | } 51 | }); 52 | cwr.with_buf(|bytes| {io.write(bytes); io.flush()}); 53 | continue; 54 | } 55 | _ => { /* fallthrough: error */ } 56 | } 57 | } 58 | else if command.as_slice() == bytes!("SET") { 59 | match (lst.len(), lst.get(1), lst.get(2)) { 60 | (3, Some(&redis::Data(ref key)), Some(&redis::Data(ref val))) => { 61 | debug!("SET: {:s} {:?}", std::str::from_utf8(key.as_slice()).unwrap(), val); 62 | shared_ht.write(|ht| ht.insert(key.clone(), val.clone())); 63 | let mut cwr = redis::CommandWriter::new(); 64 | cwr.status("OK"); 65 | cwr.with_buf(|bytes| {io.write(bytes); io.flush()}); 66 | continue; 67 | } 68 | _ => { /* fallthrough: error */ } 69 | } 70 | } 71 | else { 72 | /* fallthrough: error */ 73 | } 74 | } 75 | _ => { /* fallthrough: error */ } 76 | } 77 | } 78 | _ => { /* fallthrough: error */ } 79 | } 80 | 81 | /* error */ 82 | let mut cwr = redis::CommandWriter::new(); 83 | cwr.error("Invalid Command"); 84 | cwr.with_buf(|bytes| {io.write(bytes); io.flush()}); 85 | } 86 | } 87 | 88 | fn main() { 89 | let addr: SocketAddr = from_str("127.0.0.1:8000").unwrap(); 90 | let shared_ht = RWArc::new(HashMap::new()); 91 | 92 | match TcpListener::bind(addr) { 93 | Ok(listener) => { 94 | match listener.listen() { 95 | Ok(ref mut acceptor) => { 96 | loop { 97 | match acceptor.accept() { 98 | Ok(conn) => { 99 | let ht = shared_ht.clone(); 100 | task::spawn(proc() { 101 | handle_connection(conn, ht) 102 | }); 103 | } 104 | Err(_) => {} 105 | } 106 | } 107 | } 108 | Err(_) => {} 109 | } 110 | } 111 | Err(_) => {} 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/examples/simple/main.rs: -------------------------------------------------------------------------------- 1 | extern crate redis = "redis#0.1"; 2 | 3 | fn main() { 4 | let mut redis = redis::Client::new("127.0.0.1:6379"); 5 | redis.set_int("counter", 1); 6 | redis.set("key", "Hello"); 7 | 8 | redis.incr("counter"); 9 | 10 | let counter = redis.get_int("counter").unwrap().unwrap(); 11 | println!("counter = {}", counter); 12 | 13 | let key = redis.get_str("key").unwrap().unwrap(); 14 | println!("key = {}", key); 15 | 16 | match redis.get("key").unwrap() { 17 | redis::Nil => { 18 | println!("Key not found") 19 | } 20 | redis::Data(ref s) => { 21 | println!("{:?}", std::str::from_utf8(*s)) 22 | } 23 | _ => { fail!() } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/redis/lib.rs: -------------------------------------------------------------------------------- 1 | #[crate_id = "redis#0.1"]; 2 | #[desc = "A Rust client library for Redis"]; 3 | #[license = "MIT"]; 4 | #[crate_type = "lib"]; 5 | 6 | use std::io::{IoResult,IoError,InvalidInput}; 7 | use std::io::net::ip::SocketAddr; 8 | use std::io::net::tcp::TcpStream; 9 | use std::io::{BufferedStream, Stream}; 10 | use std::vec::bytes::push_bytes; 11 | use std::vec; 12 | use std::str::from_utf8; 13 | 14 | pub enum Result { 15 | Nil, 16 | Int(i64), 17 | Data(~[u8]), 18 | List(~[Result]), 19 | Error(~str), 20 | Status(~str) 21 | } 22 | 23 | fn invalid_input(desc: &'static str) -> IoError { 24 | IoError {kind: InvalidInput, desc: desc, detail: None} 25 | } 26 | 27 | fn read_char(io: &mut BufferedStream) -> IoResult { 28 | Ok(try!(io.read_byte()) as char) 29 | } 30 | 31 | fn parse_data(len: uint, io: &mut BufferedStream) -> IoResult { 32 | let res = 33 | if len > 0 { 34 | let bytes = try!(io.read_bytes(len)); 35 | if bytes.len() != len { 36 | return Err(invalid_input("Invalid number of bytes")) 37 | } else { 38 | Data(bytes) 39 | } 40 | } else { 41 | // XXX: needs this to be special case or is read_bytes() working with len=0? 42 | Data(~[]) 43 | }; 44 | 45 | if try!(read_char(io)) != '\r' { 46 | return Err(invalid_input("Carriage return expected")); // TODO: ignore 47 | } 48 | 49 | if try!(read_char(io)) != '\n' { 50 | return Err(invalid_input("Newline expected")); 51 | } 52 | 53 | return Ok(res); 54 | } 55 | 56 | fn parse_list(len: uint, io: &mut BufferedStream) -> IoResult { 57 | let mut list: ~[Result] = vec::with_capacity(len); 58 | 59 | for _ in range(0, len) { 60 | list.push(try!(parse(io))) 61 | } 62 | 63 | Ok(List(list)) 64 | } 65 | 66 | fn parse_int_line(io: &mut BufferedStream) -> IoResult { 67 | let mut i: i64 = 0; 68 | let mut digits: uint = 0; 69 | let mut negative: bool = false; 70 | 71 | loop { 72 | match try!(read_char(io)) { 73 | ch @ '0' .. '9' => { 74 | digits += 1; 75 | i = (i * 10) + (ch as i64 - '0' as i64); 76 | } 77 | '-' => { 78 | if negative { return Err(invalid_input("Invalid negative number")) } 79 | negative = true 80 | } 81 | '\r' => { 82 | if try!(read_char(io)) != '\n' { 83 | return Err(invalid_input("Newline expected")) 84 | } 85 | break 86 | } 87 | '\n' => { break } 88 | _ => { return Err(invalid_input("Invalid character")) } 89 | } 90 | } 91 | 92 | if digits == 0 { return Err(invalid_input("No number given")) } 93 | 94 | if negative { Ok(-i) } 95 | else { Ok(i) } 96 | } 97 | 98 | fn parse_n(io: &mut BufferedStream, f: |uint, &mut BufferedStream| -> IoResult) -> IoResult { 99 | match try!(parse_int_line(io)) { 100 | -1 => Ok(Nil), 101 | len if len >= 0 => f(len as uint, io), // XXX: i64 might be larger than uint 102 | _ => Err(invalid_input("Invalid number")) 103 | } 104 | } 105 | 106 | fn parse_status(io: &mut BufferedStream) -> IoResult { 107 | Ok(Status(try!(io.read_line()))) 108 | } 109 | 110 | fn parse_error(io: &mut BufferedStream) -> IoResult { 111 | Ok(Error(try!(io.read_line()))) 112 | } 113 | 114 | pub fn parse(io: &mut BufferedStream) -> IoResult { 115 | match try!(read_char(io)) { 116 | '$' => parse_n(io, parse_data), 117 | '*' => parse_n(io, parse_list), 118 | '+' => parse_status(io), 119 | '-' => parse_error(io), 120 | ':' => Ok(Int(try!(parse_int_line(io)))), 121 | _ => Err(invalid_input("Invalid character")) 122 | } 123 | } 124 | 125 | pub struct CommandWriter { 126 | buf: ~[u8] 127 | } 128 | 129 | impl CommandWriter { 130 | pub fn new() -> CommandWriter { 131 | CommandWriter { buf: ~[] } 132 | } 133 | 134 | pub fn args<'a>(&'a mut self, n: uint) -> &'a mut CommandWriter { 135 | self.write_char('*'); 136 | self.write_uint(n); 137 | self.write_crnl(); 138 | self 139 | } 140 | 141 | pub fn arg_bin<'a>(&'a mut self, arg: &[u8]) -> &'a mut CommandWriter { 142 | self.write_char('$'); 143 | self.write_uint(arg.len()); 144 | self.write_crnl(); 145 | self.write(arg); 146 | self.write_crnl(); 147 | self 148 | } 149 | 150 | pub fn nil(&mut self) { 151 | self.write_str("$-1"); 152 | self.write_crnl(); 153 | } 154 | 155 | pub fn arg_str<'a>(&'a mut self, arg: &str) -> &'a mut CommandWriter { 156 | self.write_char('$'); 157 | self.write_uint(arg.len()); 158 | self.write_crnl(); 159 | self.write_str(arg); 160 | self.write_crnl(); 161 | self 162 | } 163 | 164 | pub fn error(&mut self, err: &str) { 165 | self.write_char('-'); 166 | self.write_str(err); 167 | self.write_crnl(); 168 | } 169 | 170 | pub fn status(&mut self, status: &str) { 171 | self.write_char('+'); 172 | self.write_str(status); 173 | self.write_crnl(); 174 | } 175 | 176 | fn write_crnl(&mut self) { 177 | self.write_char('\r'); 178 | self.write_char('\n'); 179 | } 180 | 181 | fn write_uint(&mut self, n: uint) { 182 | if n < 10 { 183 | self.write_byte('0' as u8 + (n as u8)); 184 | } 185 | else { 186 | push_bytes(&mut self.buf, n.to_str().into_bytes()); // XXX: Optimize 187 | } 188 | } 189 | 190 | fn write_str(&mut self, s: &str) { 191 | push_bytes(&mut self.buf, s.as_bytes()); 192 | } 193 | 194 | fn write(&mut self, s: &[u8]) { 195 | push_bytes(&mut self.buf, s); 196 | } 197 | 198 | fn write_char(&mut self, s: char) { 199 | self.buf.push(s as u8); 200 | } 201 | 202 | fn write_byte(&mut self, b: u8) { 203 | self.buf.push(b); 204 | } 205 | 206 | pub fn with_buf(&self, f: |&[u8]| -> T) -> T { 207 | f(self.buf.as_slice()) 208 | } 209 | } 210 | 211 | fn execute(cmd: &[u8], io: &mut BufferedStream) -> IoResult { 212 | try!(io.write(cmd)); 213 | try!(io.flush()); 214 | parse(io) 215 | } 216 | 217 | pub struct Client { 218 | priv io: BufferedStream 219 | } 220 | 221 | impl Client { 222 | pub fn new(sock_addr: &str) -> Client { 223 | let addr = from_str::(sock_addr).unwrap(); 224 | let tcp_stream = TcpStream::connect(addr).unwrap(); 225 | Client::new_from_stream(tcp_stream) 226 | } 227 | } 228 | 229 | impl Client { 230 | pub fn new_from_stream(io: T) -> Client { 231 | Client { io: BufferedStream::new(io) } 232 | } 233 | 234 | pub fn get(&mut self, key: &str) -> IoResult { 235 | let mut cwr = CommandWriter::new(); 236 | cwr.args(2). 237 | arg_str("GET"). 238 | arg_str(key). 239 | with_buf(|cmd| execute(cmd, &mut self.io)) 240 | } 241 | 242 | pub fn get_str(&mut self, key: &str) -> IoResult> { 243 | match try!(self.get(key)) { 244 | Nil => Ok(None), 245 | Int(i) => Ok(Some(i.to_str())), 246 | Data(ref bytes) => Ok(Some(from_utf8(*bytes).unwrap().to_owned())), 247 | _ => fail!("Invalid result type from Redis") 248 | } 249 | } 250 | 251 | pub fn get_int(&mut self, key: &str) -> IoResult> { 252 | match try!(self.get(key)) { 253 | Nil => Ok(None), 254 | Data(ref bytes) => Ok(from_str(from_utf8(*bytes).unwrap())), // XXX 255 | _ => fail!("Invalid result type from Redis") 256 | } 257 | } 258 | 259 | pub fn set(&mut self, key: &str, val: &str) -> IoResult { 260 | let mut cwr = CommandWriter::new(); 261 | cwr.args(3). 262 | arg_str("SET"). 263 | arg_str(key). 264 | arg_str(val). 265 | with_buf(|cmd| execute(cmd, &mut self.io)) 266 | } 267 | 268 | pub fn set_int(&mut self, key: &str, val: i64) -> IoResult { 269 | self.set(key, val.to_str()) 270 | } 271 | 272 | pub fn incr(&mut self, key: &str) -> IoResult { 273 | let mut cwr = CommandWriter::new(); 274 | let res = try!(cwr.args(2). 275 | arg_str("INCR"). 276 | arg_str(key). 277 | with_buf(|cmd| execute(cmd, &mut self.io))); 278 | match res { 279 | Int(i) => Ok(i), 280 | _ => fail!() 281 | } 282 | } 283 | } 284 | --------------------------------------------------------------------------------