Repository files navigation

RDKit

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

The server speaks Redis RESP protocol, so you can reuse many Redis-compatible clients and tools such as:

  • redis-cli
  • redis-benchmark
  • Redic

And a lot more.

RDKit is used to power:

Code ClimateBuild Status

RDKit should work without problem on MRI 2.2+, may encounter bugs on earlier version of MRI or JRuby or Rubinus, in that case, please kindly open an issue on GitHub

Installation

Add this line to your application's Gemfile:

gem'rdkit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rdkit

Usage

Generally, you should implement one subclass for each of the 3 classes: RDKit::RESPResponder, RDKit::Core and RDKit::Server, and spawn one object for each class.

Your server object should have two instance variables @responder and @core pointed to your spawned instances.

RDKit::Server

classYourServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)@core=YourCore.new@responder=YourResponder.new(core)endendserver=YourServer.newtrap(:INT){server.stop}server.start

This will start a TCPServer on 0.0.0.0:3721 and stops when you CTRL-C.

RDKit::RESPResponder

@responder maps Redis commands to its methods and arguments, for example info will be sent to RESPResponder#info, and info all to RESPResponder#info with "all" as its first argument.

The return ruby object of each method will be marshaled as RESP strings, for example 'OK' becomes "+OK\r\n".

For example, with following implementation in your RESPResponder subclass:

defadd(a,b)a.to_i + b.to_iend

You implemented an adder using RDKit! See it in action:

$ redis-cli -p 3721
127.0.0.1:3721> add 1 2
(integer) 3
127.0.0.1:3721> add 5
(error) ERR wrong number of arguments for'add'command
127.0.0.1:3721>

The detailed algorithm can be found in resp.rb, at the time of writing it is like this:

defcompose(data)casedatawhen *%w{OKstringlistsethashzsetnone}"+#{data}\r\n"whentrue":1\r\n"whenfalse":0\r\n"whenInteger":#{data}\r\n"whenArray"*#{data.size}\r\n" + data.map{ |i| compose(i)}.joinwhenNilClass# Null Bulk String, not Null Array of "*-1\r\n""$-1\r\n"whenWrongTypeError"-WRONGTYPE #{data.message}\r\n"whenStandardError"-ERR #{data.message}\r\n"else# always Bulk String"$#{data.bytesize}\r\n#{data}\r\n"endend

RDKit::Core

You are required to implement a tick! method. RDKit will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:

deftick!save_non_critical_data!ifserver.cycles % 1000 == 0end

Examples

See examples under example folder.

Implementing a counter server

A simple counter server source code listing:

require'rdkit'# counter/version.rbmoduleCounterVERSION='0.0.1'end# counter/core.rbmoduleCounterclassCore < RDKit::Coreattr_accessor:countdefinitialize@count=0@last_tick=Time.nowend# `tick!` is called periodically by RDKitdeftick!@last_tick=Time.nowenddefincr(n)@count += nenddefintrospection{counter_version: Counter::VERSION,count: @count,last_tick: @last_tick}endendend# counter/command_runner.rbmoduleCounterclassCommandRunner < RDKit::RESPRunnerdefinitialize(counter)@counter=counterend# every public method of this class will be accessible by clientsdefcount@counter.countenddefincr(n=1)@counter.incr(n.to_i)endendend# counter/server.rbmoduleCounterclassServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)# @core is required by RDKit@core=Core.new# @runner is also required by RDKit@runner=CommandRunner.new(@core)enddefintrospectionsuper.merge(counter: @core.introspection)endendend# start serverserver=Counter::Server.newtrap(:INT){server.stop}server.start

Connect using redis-cli

$ redis-cli -p 3721
127.0.0.1:3721> count
(integer) 0
127.0.0.1:3721> incr
(integer) 1
127.0.0.1:3721> incr 10
(integer) 11
127.0.0.1:3721> count
(integer) 11
127.0.0.1:3721> info
# Server
rdkit_version:0.0.1
multiplexing_api:select
process_id:15083
tcp_port:3721
uptime_in_seconds:268
uptime_in_days:0
hz:10
# Clients
connected_clients:1
connected_clients_peak:1
# Memory
used_memory_rss:31.89M
used_memory_peak:31.89M
# Counter
counter_version:0.0.1
count:11
last_tick:2015-05-27 20:15:38 +0800
# Stats
total_connections_received:1
total_commands_processed:6
127.0.0.1:3721> xx
(error) ERR unknown command'xx'

Hint: if you are adventurous, try info all

Benchmarking with redis-benchmark

$ redis-benchmark -p 3721 incr
====== count ======
10000 requests completed in 0.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
2.27% <= 2 milliseconds
42.31% <= 3 milliseconds
63.99% <= 4 milliseconds
96.14% <= 5 milliseconds
...
99.97% <= 68 milliseconds
99.98% <= 71 milliseconds
99.99% <= 74 milliseconds
100.00% <= 77 milliseconds
13679.89 requests per second

Since it is single-threaded, the count will be correct:

127.0.0.1:3721> count
(integer) 10000

Implementing blocked commands

Some commands will be blocking: they may either depend on external services or need some background tasks to be run.

The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.

Therefore we introduce Server#blocking methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.

Example: see examples/blocking folder.

# blocking/command_runner.rbmoduleBlockingclassCommandRunner < RDKit::RESPRunnerattr_reader:coredefinitialize(core)@core=coreenddefblock_with_callbackcore.block_with_callback# this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned'OK'enddefblockcore.block'OK'enddefnonblockcore.nonblock'OK'endendend# blocking/core.rbmoduleBlockingclassCore < RDKit::Coredefblock_with_callbackon_success=lambda{'success'}server.blocking(on_success){do_something}enddefblockserver.blocking{do_something}enddefnonblockdo_somethingenddefdo_somethingsleep1enddeftick!endendend

Running:

$ redis-cli -p 3721
127.0.0.1:3721> block
OK
(1.03s)
127.0.0.1:3721> nonblock
OK
(1.01s)
127.0.0.1:3721> block_with_callback
"success"
(1.02s)

Benchmarking:

$ redis-benchmark -p 3721 -n 10 block
====== block ======
10 requests completed in 1.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1027 milliseconds
100.00% <= 1027 milliseconds
9.73 requests per second
$ redis-benchmark -p 3721 -n 10 nonblock
====== nonblock ======
10 requests completed in 10.04 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1001 milliseconds
20.00% <= 2005 milliseconds
30.00% <= 3010 milliseconds
40.00% <= 4013 milliseconds
50.00% <= 5018 milliseconds
60.00% <= 6022 milliseconds
70.00% <= 7027 milliseconds
80.00% <= 8030 milliseconds
90.00% <= 9034 milliseconds
100.00% <= 10039 milliseconds
1.00 requests per second

See the difference between blocking and non-blocking commands?

Additional IO Handler Injection

Since RDKit version 0.1.5, it allows injection of additional IO handlers into the main loop.

For examples, please refer to examples/ioinject for an injected UDP echo server.

Implemented Redis Commands

commandsupportnote
infofulladditional objspace and gc commands
pingfull
echofull
timefull
selectpartial/compatibleredis-benchmark requires select command
configget, set, resetstat
slowlogfull
clientgetname, setname, list, killkill filter only supports id, addr
monitorfull
debugsleep, segfault
shutdownfull
getfull
setwithout options
delfull
keyswithout pattern (return all)
lpushfull
lpopfull
rpopfull
llenfull
lrangepartial (not fully tested)
existsfull
flushdbfull
flushallfull
mgetfull
msetfull
strlenfull
saddfull
scardfull
smembersfull
sismemberfull
sremfull
hsetfull
hgetfull
hexistsfull
hlenfull
hstrlenfull
hdelfull
hkeysfull
hvalsfull
setnxfull
getsetfull

Implemented Additional Commands

commanddescription
gcstart garbage collection immediately
heapdumpObjectSpace.dump_all to ./tmp

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

Contributing

  1. Fork it ( https://github.com/forresty/rdkit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

RDKit

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

The server speaks Redis RESP protocol, so you can reuse many Redis-compatible clients and tools such as:

  • redis-cli
  • redis-benchmark
  • Redic

And a lot more.

RDKit is used to power:

Code ClimateBuild Status

RDKit should work without problem on MRI 2.2+, may encounter bugs on earlier version of MRI or JRuby or Rubinus, in that case, please kindly open an issue on GitHub

Installation

Add this line to your application's Gemfile:

gem'rdkit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rdkit

Usage

Generally, you should implement one subclass for each of the 3 classes: RDKit::RESPResponder, RDKit::Core and RDKit::Server, and spawn one object for each class.

Your server object should have two instance variables @responder and @core pointed to your spawned instances.

RDKit::Server

classYourServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)@core=YourCore.new@responder=YourResponder.new(core)endendserver=YourServer.newtrap(:INT){server.stop}server.start

This will start a TCPServer on 0.0.0.0:3721 and stops when you CTRL-C.

RDKit::RESPResponder

@responder maps Redis commands to its methods and arguments, for example info will be sent to RESPResponder#info, and info all to RESPResponder#info with "all" as its first argument.

The return ruby object of each method will be marshaled as RESP strings, for example 'OK' becomes "+OK\r\n".

For example, with following implementation in your RESPResponder subclass:

defadd(a,b)a.to_i + b.to_iend

You implemented an adder using RDKit! See it in action:

$ redis-cli -p 3721
127.0.0.1:3721> add 1 2
(integer) 3
127.0.0.1:3721> add 5
(error) ERR wrong number of arguments for'add'command
127.0.0.1:3721>

The detailed algorithm can be found in resp.rb, at the time of writing it is like this:

defcompose(data)casedatawhen *%w{OKstringlistsethashzsetnone}"+#{data}\r\n"whentrue":1\r\n"whenfalse":0\r\n"whenInteger":#{data}\r\n"whenArray"*#{data.size}\r\n" + data.map{ |i| compose(i)}.joinwhenNilClass# Null Bulk String, not Null Array of "*-1\r\n""$-1\r\n"whenWrongTypeError"-WRONGTYPE #{data.message}\r\n"whenStandardError"-ERR #{data.message}\r\n"else# always Bulk String"$#{data.bytesize}\r\n#{data}\r\n"endend

RDKit::Core

You are required to implement a tick! method. RDKit will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:

deftick!save_non_critical_data!ifserver.cycles % 1000 == 0end

Examples

See examples under example folder.

Implementing a counter server

A simple counter server source code listing:

require'rdkit'# counter/version.rbmoduleCounterVERSION='0.0.1'end# counter/core.rbmoduleCounterclassCore < RDKit::Coreattr_accessor:countdefinitialize@count=0@last_tick=Time.nowend# `tick!` is called periodically by RDKitdeftick!@last_tick=Time.nowenddefincr(n)@count += nenddefintrospection{counter_version: Counter::VERSION,count: @count,last_tick: @last_tick}endendend# counter/command_runner.rbmoduleCounterclassCommandRunner < RDKit::RESPRunnerdefinitialize(counter)@counter=counterend# every public method of this class will be accessible by clientsdefcount@counter.countenddefincr(n=1)@counter.incr(n.to_i)endendend# counter/server.rbmoduleCounterclassServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)# @core is required by RDKit@core=Core.new# @runner is also required by RDKit@runner=CommandRunner.new(@core)enddefintrospectionsuper.merge(counter: @core.introspection)endendend# start serverserver=Counter::Server.newtrap(:INT){server.stop}server.start

Connect using redis-cli

$ redis-cli -p 3721
127.0.0.1:3721> count
(integer) 0
127.0.0.1:3721> incr
(integer) 1
127.0.0.1:3721> incr 10
(integer) 11
127.0.0.1:3721> count
(integer) 11
127.0.0.1:3721> info
# Server
rdkit_version:0.0.1
multiplexing_api:select
process_id:15083
tcp_port:3721
uptime_in_seconds:268
uptime_in_days:0
hz:10
# Clients
connected_clients:1
connected_clients_peak:1
# Memory
used_memory_rss:31.89M
used_memory_peak:31.89M
# Counter
counter_version:0.0.1
count:11
last_tick:2015-05-27 20:15:38 +0800
# Stats
total_connections_received:1
total_commands_processed:6
127.0.0.1:3721> xx
(error) ERR unknown command'xx'

Hint: if you are adventurous, try info all

Benchmarking with redis-benchmark

$ redis-benchmark -p 3721 incr
====== count ======
10000 requests completed in 0.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
2.27% <= 2 milliseconds
42.31% <= 3 milliseconds
63.99% <= 4 milliseconds
96.14% <= 5 milliseconds
...
99.97% <= 68 milliseconds
99.98% <= 71 milliseconds
99.99% <= 74 milliseconds
100.00% <= 77 milliseconds
13679.89 requests per second

Since it is single-threaded, the count will be correct:

127.0.0.1:3721> count
(integer) 10000

Implementing blocked commands

Some commands will be blocking: they may either depend on external services or need some background tasks to be run.

The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.

Therefore we introduce Server#blocking methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.

Example: see examples/blocking folder.

# blocking/command_runner.rbmoduleBlockingclassCommandRunner < RDKit::RESPRunnerattr_reader:coredefinitialize(core)@core=coreenddefblock_with_callbackcore.block_with_callback# this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned'OK'enddefblockcore.block'OK'enddefnonblockcore.nonblock'OK'endendend# blocking/core.rbmoduleBlockingclassCore < RDKit::Coredefblock_with_callbackon_success=lambda{'success'}server.blocking(on_success){do_something}enddefblockserver.blocking{do_something}enddefnonblockdo_somethingenddefdo_somethingsleep1enddeftick!endendend

Running:

$ redis-cli -p 3721
127.0.0.1:3721> block
OK
(1.03s)
127.0.0.1:3721> nonblock
OK
(1.01s)
127.0.0.1:3721> block_with_callback
"success"
(1.02s)

Benchmarking:

$ redis-benchmark -p 3721 -n 10 block
====== block ======
10 requests completed in 1.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1027 milliseconds
100.00% <= 1027 milliseconds
9.73 requests per second
$ redis-benchmark -p 3721 -n 10 nonblock
====== nonblock ======
10 requests completed in 10.04 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1001 milliseconds
20.00% <= 2005 milliseconds
30.00% <= 3010 milliseconds
40.00% <= 4013 milliseconds
50.00% <= 5018 milliseconds
60.00% <= 6022 milliseconds
70.00% <= 7027 milliseconds
80.00% <= 8030 milliseconds
90.00% <= 9034 milliseconds
100.00% <= 10039 milliseconds
1.00 requests per second

See the difference between blocking and non-blocking commands?

Additional IO Handler Injection

Since RDKit version 0.1.5, it allows injection of additional IO handlers into the main loop.

For examples, please refer to examples/ioinject for an injected UDP echo server.

Implemented Redis Commands

commandsupportnote
infofulladditional objspace and gc commands
pingfull
echofull
timefull
selectpartial/compatibleredis-benchmark requires select command
configget, set, resetstat
slowlogfull
clientgetname, setname, list, killkill filter only supports id, addr
monitorfull
debugsleep, segfault
shutdownfull
getfull
setwithout options
delfull
keyswithout pattern (return all)
lpushfull
lpopfull
rpopfull
llenfull
lrangepartial (not fully tested)
existsfull
flushdbfull
flushallfull
mgetfull
msetfull
strlenfull
saddfull
scardfull
smembersfull
sismemberfull
sremfull
hsetfull
hgetfull
hexistsfull
hlenfull
hstrlenfull
hdelfull
hkeysfull
hvalsfull
setnxfull
getsetfull

Implemented Additional Commands

commanddescription
gcstart garbage collection immediately
heapdumpObjectSpace.dump_all to ./tmp

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

Contributing

  1. Fork it ( https://github.com/forresty/rdkit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

RDKit

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

The server speaks Redis RESP protocol, so you can reuse many Redis-compatible clients and tools such as:

  • redis-cli
  • redis-benchmark
  • Redic

And a lot more.

RDKit is used to power:

Code ClimateBuild Status

RDKit should work without problem on MRI 2.2+, may encounter bugs on earlier version of MRI or JRuby or Rubinus, in that case, please kindly open an issue on GitHub

Installation

Add this line to your application's Gemfile:

gem'rdkit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rdkit

Usage

Generally, you should implement one subclass for each of the 3 classes: RDKit::RESPResponder, RDKit::Core and RDKit::Server, and spawn one object for each class.

Your server object should have two instance variables @responder and @core pointed to your spawned instances.

RDKit::Server

classYourServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)@core=YourCore.new@responder=YourResponder.new(core)endendserver=YourServer.newtrap(:INT){server.stop}server.start

This will start a TCPServer on 0.0.0.0:3721 and stops when you CTRL-C.

RDKit::RESPResponder

@responder maps Redis commands to its methods and arguments, for example info will be sent to RESPResponder#info, and info all to RESPResponder#info with "all" as its first argument.

The return ruby object of each method will be marshaled as RESP strings, for example 'OK' becomes "+OK\r\n".

For example, with following implementation in your RESPResponder subclass:

defadd(a,b)a.to_i + b.to_iend

You implemented an adder using RDKit! See it in action:

$ redis-cli -p 3721
127.0.0.1:3721> add 1 2
(integer) 3
127.0.0.1:3721> add 5
(error) ERR wrong number of arguments for'add'command
127.0.0.1:3721>

The detailed algorithm can be found in resp.rb, at the time of writing it is like this:

defcompose(data)casedatawhen *%w{OKstringlistsethashzsetnone}"+#{data}\r\n"whentrue":1\r\n"whenfalse":0\r\n"whenInteger":#{data}\r\n"whenArray"*#{data.size}\r\n" + data.map{ |i| compose(i)}.joinwhenNilClass# Null Bulk String, not Null Array of "*-1\r\n""$-1\r\n"whenWrongTypeError"-WRONGTYPE #{data.message}\r\n"whenStandardError"-ERR #{data.message}\r\n"else# always Bulk String"$#{data.bytesize}\r\n#{data}\r\n"endend

RDKit::Core

You are required to implement a tick! method. RDKit will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:

deftick!save_non_critical_data!ifserver.cycles % 1000 == 0end

Examples

See examples under example folder.

Implementing a counter server

A simple counter server source code listing:

require'rdkit'# counter/version.rbmoduleCounterVERSION='0.0.1'end# counter/core.rbmoduleCounterclassCore < RDKit::Coreattr_accessor:countdefinitialize@count=0@last_tick=Time.nowend# `tick!` is called periodically by RDKitdeftick!@last_tick=Time.nowenddefincr(n)@count += nenddefintrospection{counter_version: Counter::VERSION,count: @count,last_tick: @last_tick}endendend# counter/command_runner.rbmoduleCounterclassCommandRunner < RDKit::RESPRunnerdefinitialize(counter)@counter=counterend# every public method of this class will be accessible by clientsdefcount@counter.countenddefincr(n=1)@counter.incr(n.to_i)endendend# counter/server.rbmoduleCounterclassServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)# @core is required by RDKit@core=Core.new# @runner is also required by RDKit@runner=CommandRunner.new(@core)enddefintrospectionsuper.merge(counter: @core.introspection)endendend# start serverserver=Counter::Server.newtrap(:INT){server.stop}server.start

Connect using redis-cli

$ redis-cli -p 3721
127.0.0.1:3721> count
(integer) 0
127.0.0.1:3721> incr
(integer) 1
127.0.0.1:3721> incr 10
(integer) 11
127.0.0.1:3721> count
(integer) 11
127.0.0.1:3721> info
# Server
rdkit_version:0.0.1
multiplexing_api:select
process_id:15083
tcp_port:3721
uptime_in_seconds:268
uptime_in_days:0
hz:10
# Clients
connected_clients:1
connected_clients_peak:1
# Memory
used_memory_rss:31.89M
used_memory_peak:31.89M
# Counter
counter_version:0.0.1
count:11
last_tick:2015-05-27 20:15:38 +0800
# Stats
total_connections_received:1
total_commands_processed:6
127.0.0.1:3721> xx
(error) ERR unknown command'xx'

Hint: if you are adventurous, try info all

Benchmarking with redis-benchmark

$ redis-benchmark -p 3721 incr
====== count ======
10000 requests completed in 0.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
2.27% <= 2 milliseconds
42.31% <= 3 milliseconds
63.99% <= 4 milliseconds
96.14% <= 5 milliseconds
...
99.97% <= 68 milliseconds
99.98% <= 71 milliseconds
99.99% <= 74 milliseconds
100.00% <= 77 milliseconds
13679.89 requests per second

Since it is single-threaded, the count will be correct:

127.0.0.1:3721> count
(integer) 10000

Implementing blocked commands

Some commands will be blocking: they may either depend on external services or need some background tasks to be run.

The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.

Therefore we introduce Server#blocking methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.

Example: see examples/blocking folder.

# blocking/command_runner.rbmoduleBlockingclassCommandRunner < RDKit::RESPRunnerattr_reader:coredefinitialize(core)@core=coreenddefblock_with_callbackcore.block_with_callback# this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned'OK'enddefblockcore.block'OK'enddefnonblockcore.nonblock'OK'endendend# blocking/core.rbmoduleBlockingclassCore < RDKit::Coredefblock_with_callbackon_success=lambda{'success'}server.blocking(on_success){do_something}enddefblockserver.blocking{do_something}enddefnonblockdo_somethingenddefdo_somethingsleep1enddeftick!endendend

Running:

$ redis-cli -p 3721
127.0.0.1:3721> block
OK
(1.03s)
127.0.0.1:3721> nonblock
OK
(1.01s)
127.0.0.1:3721> block_with_callback
"success"
(1.02s)

Benchmarking:

$ redis-benchmark -p 3721 -n 10 block
====== block ======
10 requests completed in 1.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1027 milliseconds
100.00% <= 1027 milliseconds
9.73 requests per second
$ redis-benchmark -p 3721 -n 10 nonblock
====== nonblock ======
10 requests completed in 10.04 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1001 milliseconds
20.00% <= 2005 milliseconds
30.00% <= 3010 milliseconds
40.00% <= 4013 milliseconds
50.00% <= 5018 milliseconds
60.00% <= 6022 milliseconds
70.00% <= 7027 milliseconds
80.00% <= 8030 milliseconds
90.00% <= 9034 milliseconds
100.00% <= 10039 milliseconds
1.00 requests per second

See the difference between blocking and non-blocking commands?

Additional IO Handler Injection

Since RDKit version 0.1.5, it allows injection of additional IO handlers into the main loop.

For examples, please refer to examples/ioinject for an injected UDP echo server.

Implemented Redis Commands

commandsupportnote
infofulladditional objspace and gc commands
pingfull
echofull
timefull
selectpartial/compatibleredis-benchmark requires select command
configget, set, resetstat
slowlogfull
clientgetname, setname, list, killkill filter only supports id, addr
monitorfull
debugsleep, segfault
shutdownfull
getfull
setwithout options
delfull
keyswithout pattern (return all)
lpushfull
lpopfull
rpopfull
llenfull
lrangepartial (not fully tested)
existsfull
flushdbfull
flushallfull
mgetfull
msetfull
strlenfull
saddfull
scardfull
smembersfull
sismemberfull
sremfull
hsetfull
hgetfull
hexistsfull
hlenfull
hstrlenfull
hdelfull
hkeysfull
hvalsfull
setnxfull
getsetfull

Implemented Additional Commands

commanddescription
gcstart garbage collection immediately
heapdumpObjectSpace.dump_all to ./tmp

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

Contributing

  1. Fork it ( https://github.com/forresty/rdkit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

RDKit

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

The server speaks Redis RESP protocol, so you can reuse many Redis-compatible clients and tools such as:

  • redis-cli
  • redis-benchmark
  • Redic

And a lot more.

RDKit is used to power:

Code ClimateBuild Status

RDKit should work without problem on MRI 2.2+, may encounter bugs on earlier version of MRI or JRuby or Rubinus, in that case, please kindly open an issue on GitHub

Installation

Add this line to your application's Gemfile:

gem'rdkit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rdkit

Usage

Generally, you should implement one subclass for each of the 3 classes: RDKit::RESPResponder, RDKit::Core and RDKit::Server, and spawn one object for each class.

Your server object should have two instance variables @responder and @core pointed to your spawned instances.

RDKit::Server

classYourServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)@core=YourCore.new@responder=YourResponder.new(core)endendserver=YourServer.newtrap(:INT){server.stop}server.start

This will start a TCPServer on 0.0.0.0:3721 and stops when you CTRL-C.

RDKit::RESPResponder

@responder maps Redis commands to its methods and arguments, for example info will be sent to RESPResponder#info, and info all to RESPResponder#info with "all" as its first argument.

The return ruby object of each method will be marshaled as RESP strings, for example 'OK' becomes "+OK\r\n".

For example, with following implementation in your RESPResponder subclass:

defadd(a,b)a.to_i + b.to_iend

You implemented an adder using RDKit! See it in action:

$ redis-cli -p 3721
127.0.0.1:3721> add 1 2
(integer) 3
127.0.0.1:3721> add 5
(error) ERR wrong number of arguments for'add'command
127.0.0.1:3721>

The detailed algorithm can be found in resp.rb, at the time of writing it is like this:

defcompose(data)casedatawhen *%w{OKstringlistsethashzsetnone}"+#{data}\r\n"whentrue":1\r\n"whenfalse":0\r\n"whenInteger":#{data}\r\n"whenArray"*#{data.size}\r\n" + data.map{ |i| compose(i)}.joinwhenNilClass# Null Bulk String, not Null Array of "*-1\r\n""$-1\r\n"whenWrongTypeError"-WRONGTYPE #{data.message}\r\n"whenStandardError"-ERR #{data.message}\r\n"else# always Bulk String"$#{data.bytesize}\r\n#{data}\r\n"endend

RDKit::Core

You are required to implement a tick! method. RDKit will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:

deftick!save_non_critical_data!ifserver.cycles % 1000 == 0end

Examples

See examples under example folder.

Implementing a counter server

A simple counter server source code listing:

require'rdkit'# counter/version.rbmoduleCounterVERSION='0.0.1'end# counter/core.rbmoduleCounterclassCore < RDKit::Coreattr_accessor:countdefinitialize@count=0@last_tick=Time.nowend# `tick!` is called periodically by RDKitdeftick!@last_tick=Time.nowenddefincr(n)@count += nenddefintrospection{counter_version: Counter::VERSION,count: @count,last_tick: @last_tick}endendend# counter/command_runner.rbmoduleCounterclassCommandRunner < RDKit::RESPRunnerdefinitialize(counter)@counter=counterend# every public method of this class will be accessible by clientsdefcount@counter.countenddefincr(n=1)@counter.incr(n.to_i)endendend# counter/server.rbmoduleCounterclassServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)# @core is required by RDKit@core=Core.new# @runner is also required by RDKit@runner=CommandRunner.new(@core)enddefintrospectionsuper.merge(counter: @core.introspection)endendend# start serverserver=Counter::Server.newtrap(:INT){server.stop}server.start

Connect using redis-cli

$ redis-cli -p 3721
127.0.0.1:3721> count
(integer) 0
127.0.0.1:3721> incr
(integer) 1
127.0.0.1:3721> incr 10
(integer) 11
127.0.0.1:3721> count
(integer) 11
127.0.0.1:3721> info
# Server
rdkit_version:0.0.1
multiplexing_api:select
process_id:15083
tcp_port:3721
uptime_in_seconds:268
uptime_in_days:0
hz:10
# Clients
connected_clients:1
connected_clients_peak:1
# Memory
used_memory_rss:31.89M
used_memory_peak:31.89M
# Counter
counter_version:0.0.1
count:11
last_tick:2015-05-27 20:15:38 +0800
# Stats
total_connections_received:1
total_commands_processed:6
127.0.0.1:3721> xx
(error) ERR unknown command'xx'

Hint: if you are adventurous, try info all

Benchmarking with redis-benchmark

$ redis-benchmark -p 3721 incr
====== count ======
10000 requests completed in 0.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
2.27% <= 2 milliseconds
42.31% <= 3 milliseconds
63.99% <= 4 milliseconds
96.14% <= 5 milliseconds
...
99.97% <= 68 milliseconds
99.98% <= 71 milliseconds
99.99% <= 74 milliseconds
100.00% <= 77 milliseconds
13679.89 requests per second

Since it is single-threaded, the count will be correct:

127.0.0.1:3721> count
(integer) 10000

Implementing blocked commands

Some commands will be blocking: they may either depend on external services or need some background tasks to be run.

The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.

Therefore we introduce Server#blocking methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.

Example: see examples/blocking folder.

# blocking/command_runner.rbmoduleBlockingclassCommandRunner < RDKit::RESPRunnerattr_reader:coredefinitialize(core)@core=coreenddefblock_with_callbackcore.block_with_callback# this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned'OK'enddefblockcore.block'OK'enddefnonblockcore.nonblock'OK'endendend# blocking/core.rbmoduleBlockingclassCore < RDKit::Coredefblock_with_callbackon_success=lambda{'success'}server.blocking(on_success){do_something}enddefblockserver.blocking{do_something}enddefnonblockdo_somethingenddefdo_somethingsleep1enddeftick!endendend

Running:

$ redis-cli -p 3721
127.0.0.1:3721> block
OK
(1.03s)
127.0.0.1:3721> nonblock
OK
(1.01s)
127.0.0.1:3721> block_with_callback
"success"
(1.02s)

Benchmarking:

$ redis-benchmark -p 3721 -n 10 block
====== block ======
10 requests completed in 1.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1027 milliseconds
100.00% <= 1027 milliseconds
9.73 requests per second
$ redis-benchmark -p 3721 -n 10 nonblock
====== nonblock ======
10 requests completed in 10.04 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1001 milliseconds
20.00% <= 2005 milliseconds
30.00% <= 3010 milliseconds
40.00% <= 4013 milliseconds
50.00% <= 5018 milliseconds
60.00% <= 6022 milliseconds
70.00% <= 7027 milliseconds
80.00% <= 8030 milliseconds
90.00% <= 9034 milliseconds
100.00% <= 10039 milliseconds
1.00 requests per second

See the difference between blocking and non-blocking commands?

Additional IO Handler Injection

Since RDKit version 0.1.5, it allows injection of additional IO handlers into the main loop.

For examples, please refer to examples/ioinject for an injected UDP echo server.

Implemented Redis Commands

commandsupportnote
infofulladditional objspace and gc commands
pingfull
echofull
timefull
selectpartial/compatibleredis-benchmark requires select command
configget, set, resetstat
slowlogfull
clientgetname, setname, list, killkill filter only supports id, addr
monitorfull
debugsleep, segfault
shutdownfull
getfull
setwithout options
delfull
keyswithout pattern (return all)
lpushfull
lpopfull
rpopfull
llenfull
lrangepartial (not fully tested)
existsfull
flushdbfull
flushallfull
mgetfull
msetfull
strlenfull
saddfull
scardfull
smembersfull
sismemberfull
sremfull
hsetfull
hgetfull
hexistsfull
hlenfull
hstrlenfull
hdelfull
hkeysfull
hvalsfull
setnxfull
getsetfull

Implemented Additional Commands

commanddescription
gcstart garbage collection immediately
heapdumpObjectSpace.dump_all to ./tmp

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

Contributing

  1. Fork it ( https://github.com/forresty/rdkit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

RDKit

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

The server speaks Redis RESP protocol, so you can reuse many Redis-compatible clients and tools such as:

  • redis-cli
  • redis-benchmark
  • Redic

And a lot more.

RDKit is used to power:

Code ClimateBuild Status

RDKit should work without problem on MRI 2.2+, may encounter bugs on earlier version of MRI or JRuby or Rubinus, in that case, please kindly open an issue on GitHub

Installation

Add this line to your application's Gemfile:

gem'rdkit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rdkit

Usage

Generally, you should implement one subclass for each of the 3 classes: RDKit::RESPResponder, RDKit::Core and RDKit::Server, and spawn one object for each class.

Your server object should have two instance variables @responder and @core pointed to your spawned instances.

RDKit::Server

classYourServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)@core=YourCore.new@responder=YourResponder.new(core)endendserver=YourServer.newtrap(:INT){server.stop}server.start

This will start a TCPServer on 0.0.0.0:3721 and stops when you CTRL-C.

RDKit::RESPResponder

@responder maps Redis commands to its methods and arguments, for example info will be sent to RESPResponder#info, and info all to RESPResponder#info with "all" as its first argument.

The return ruby object of each method will be marshaled as RESP strings, for example 'OK' becomes "+OK\r\n".

For example, with following implementation in your RESPResponder subclass:

defadd(a,b)a.to_i + b.to_iend

You implemented an adder using RDKit! See it in action:

$ redis-cli -p 3721
127.0.0.1:3721> add 1 2
(integer) 3
127.0.0.1:3721> add 5
(error) ERR wrong number of arguments for'add'command
127.0.0.1:3721>

The detailed algorithm can be found in resp.rb, at the time of writing it is like this:

defcompose(data)casedatawhen *%w{OKstringlistsethashzsetnone}"+#{data}\r\n"whentrue":1\r\n"whenfalse":0\r\n"whenInteger":#{data}\r\n"whenArray"*#{data.size}\r\n" + data.map{ |i| compose(i)}.joinwhenNilClass# Null Bulk String, not Null Array of "*-1\r\n""$-1\r\n"whenWrongTypeError"-WRONGTYPE #{data.message}\r\n"whenStandardError"-ERR #{data.message}\r\n"else# always Bulk String"$#{data.bytesize}\r\n#{data}\r\n"endend

RDKit::Core

You are required to implement a tick! method. RDKit will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:

deftick!save_non_critical_data!ifserver.cycles % 1000 == 0end

Examples

See examples under example folder.

Implementing a counter server

A simple counter server source code listing:

require'rdkit'# counter/version.rbmoduleCounterVERSION='0.0.1'end# counter/core.rbmoduleCounterclassCore < RDKit::Coreattr_accessor:countdefinitialize@count=0@last_tick=Time.nowend# `tick!` is called periodically by RDKitdeftick!@last_tick=Time.nowenddefincr(n)@count += nenddefintrospection{counter_version: Counter::VERSION,count: @count,last_tick: @last_tick}endendend# counter/command_runner.rbmoduleCounterclassCommandRunner < RDKit::RESPRunnerdefinitialize(counter)@counter=counterend# every public method of this class will be accessible by clientsdefcount@counter.countenddefincr(n=1)@counter.incr(n.to_i)endendend# counter/server.rbmoduleCounterclassServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)# @core is required by RDKit@core=Core.new# @runner is also required by RDKit@runner=CommandRunner.new(@core)enddefintrospectionsuper.merge(counter: @core.introspection)endendend# start serverserver=Counter::Server.newtrap(:INT){server.stop}server.start

Connect using redis-cli

$ redis-cli -p 3721
127.0.0.1:3721> count
(integer) 0
127.0.0.1:3721> incr
(integer) 1
127.0.0.1:3721> incr 10
(integer) 11
127.0.0.1:3721> count
(integer) 11
127.0.0.1:3721> info
# Server
rdkit_version:0.0.1
multiplexing_api:select
process_id:15083
tcp_port:3721
uptime_in_seconds:268
uptime_in_days:0
hz:10
# Clients
connected_clients:1
connected_clients_peak:1
# Memory
used_memory_rss:31.89M
used_memory_peak:31.89M
# Counter
counter_version:0.0.1
count:11
last_tick:2015-05-27 20:15:38 +0800
# Stats
total_connections_received:1
total_commands_processed:6
127.0.0.1:3721> xx
(error) ERR unknown command'xx'

Hint: if you are adventurous, try info all

Benchmarking with redis-benchmark

$ redis-benchmark -p 3721 incr
====== count ======
10000 requests completed in 0.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
2.27% <= 2 milliseconds
42.31% <= 3 milliseconds
63.99% <= 4 milliseconds
96.14% <= 5 milliseconds
...
99.97% <= 68 milliseconds
99.98% <= 71 milliseconds
99.99% <= 74 milliseconds
100.00% <= 77 milliseconds
13679.89 requests per second

Since it is single-threaded, the count will be correct:

127.0.0.1:3721> count
(integer) 10000

Implementing blocked commands

Some commands will be blocking: they may either depend on external services or need some background tasks to be run.

The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.

Therefore we introduce Server#blocking methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.

Example: see examples/blocking folder.

# blocking/command_runner.rbmoduleBlockingclassCommandRunner < RDKit::RESPRunnerattr_reader:coredefinitialize(core)@core=coreenddefblock_with_callbackcore.block_with_callback# this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned'OK'enddefblockcore.block'OK'enddefnonblockcore.nonblock'OK'endendend# blocking/core.rbmoduleBlockingclassCore < RDKit::Coredefblock_with_callbackon_success=lambda{'success'}server.blocking(on_success){do_something}enddefblockserver.blocking{do_something}enddefnonblockdo_somethingenddefdo_somethingsleep1enddeftick!endendend

Running:

$ redis-cli -p 3721
127.0.0.1:3721> block
OK
(1.03s)
127.0.0.1:3721> nonblock
OK
(1.01s)
127.0.0.1:3721> block_with_callback
"success"
(1.02s)

Benchmarking:

$ redis-benchmark -p 3721 -n 10 block
====== block ======
10 requests completed in 1.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1027 milliseconds
100.00% <= 1027 milliseconds
9.73 requests per second
$ redis-benchmark -p 3721 -n 10 nonblock
====== nonblock ======
10 requests completed in 10.04 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1001 milliseconds
20.00% <= 2005 milliseconds
30.00% <= 3010 milliseconds
40.00% <= 4013 milliseconds
50.00% <= 5018 milliseconds
60.00% <= 6022 milliseconds
70.00% <= 7027 milliseconds
80.00% <= 8030 milliseconds
90.00% <= 9034 milliseconds
100.00% <= 10039 milliseconds
1.00 requests per second

See the difference between blocking and non-blocking commands?

Additional IO Handler Injection

Since RDKit version 0.1.5, it allows injection of additional IO handlers into the main loop.

For examples, please refer to examples/ioinject for an injected UDP echo server.

Implemented Redis Commands

commandsupportnote
infofulladditional objspace and gc commands
pingfull
echofull
timefull
selectpartial/compatibleredis-benchmark requires select command
configget, set, resetstat
slowlogfull
clientgetname, setname, list, killkill filter only supports id, addr
monitorfull
debugsleep, segfault
shutdownfull
getfull
setwithout options
delfull
keyswithout pattern (return all)
lpushfull
lpopfull
rpopfull
llenfull
lrangepartial (not fully tested)
existsfull
flushdbfull
flushallfull
mgetfull
msetfull
strlenfull
saddfull
scardfull
smembersfull
sismemberfull
sremfull
hsetfull
hgetfull
hexistsfull
hlenfull
hstrlenfull
hdelfull
hkeysfull
hvalsfull
setnxfull
getsetfull

Implemented Additional Commands

commanddescription
gcstart garbage collection immediately
heapdumpObjectSpace.dump_all to ./tmp

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

Contributing

  1. Fork it ( https://github.com/forresty/rdkit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

RDKit

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

The server speaks Redis RESP protocol, so you can reuse many Redis-compatible clients and tools such as:

  • redis-cli
  • redis-benchmark
  • Redic

And a lot more.

RDKit is used to power:

Code ClimateBuild Status

RDKit should work without problem on MRI 2.2+, may encounter bugs on earlier version of MRI or JRuby or Rubinus, in that case, please kindly open an issue on GitHub

Installation

Add this line to your application's Gemfile:

gem'rdkit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rdkit

Usage

Generally, you should implement one subclass for each of the 3 classes: RDKit::RESPResponder, RDKit::Core and RDKit::Server, and spawn one object for each class.

Your server object should have two instance variables @responder and @core pointed to your spawned instances.

RDKit::Server

classYourServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)@core=YourCore.new@responder=YourResponder.new(core)endendserver=YourServer.newtrap(:INT){server.stop}server.start

This will start a TCPServer on 0.0.0.0:3721 and stops when you CTRL-C.

RDKit::RESPResponder

@responder maps Redis commands to its methods and arguments, for example info will be sent to RESPResponder#info, and info all to RESPResponder#info with "all" as its first argument.

The return ruby object of each method will be marshaled as RESP strings, for example 'OK' becomes "+OK\r\n".

For example, with following implementation in your RESPResponder subclass:

defadd(a,b)a.to_i + b.to_iend

You implemented an adder using RDKit! See it in action:

$ redis-cli -p 3721
127.0.0.1:3721> add 1 2
(integer) 3
127.0.0.1:3721> add 5
(error) ERR wrong number of arguments for'add'command
127.0.0.1:3721>

The detailed algorithm can be found in resp.rb, at the time of writing it is like this:

defcompose(data)casedatawhen *%w{OKstringlistsethashzsetnone}"+#{data}\r\n"whentrue":1\r\n"whenfalse":0\r\n"whenInteger":#{data}\r\n"whenArray"*#{data.size}\r\n" + data.map{ |i| compose(i)}.joinwhenNilClass# Null Bulk String, not Null Array of "*-1\r\n""$-1\r\n"whenWrongTypeError"-WRONGTYPE #{data.message}\r\n"whenStandardError"-ERR #{data.message}\r\n"else# always Bulk String"$#{data.bytesize}\r\n#{data}\r\n"endend

RDKit::Core

You are required to implement a tick! method. RDKit will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:

deftick!save_non_critical_data!ifserver.cycles % 1000 == 0end

Examples

See examples under example folder.

Implementing a counter server

A simple counter server source code listing:

require'rdkit'# counter/version.rbmoduleCounterVERSION='0.0.1'end# counter/core.rbmoduleCounterclassCore < RDKit::Coreattr_accessor:countdefinitialize@count=0@last_tick=Time.nowend# `tick!` is called periodically by RDKitdeftick!@last_tick=Time.nowenddefincr(n)@count += nenddefintrospection{counter_version: Counter::VERSION,count: @count,last_tick: @last_tick}endendend# counter/command_runner.rbmoduleCounterclassCommandRunner < RDKit::RESPRunnerdefinitialize(counter)@counter=counterend# every public method of this class will be accessible by clientsdefcount@counter.countenddefincr(n=1)@counter.incr(n.to_i)endendend# counter/server.rbmoduleCounterclassServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)# @core is required by RDKit@core=Core.new# @runner is also required by RDKit@runner=CommandRunner.new(@core)enddefintrospectionsuper.merge(counter: @core.introspection)endendend# start serverserver=Counter::Server.newtrap(:INT){server.stop}server.start

Connect using redis-cli

$ redis-cli -p 3721
127.0.0.1:3721> count
(integer) 0
127.0.0.1:3721> incr
(integer) 1
127.0.0.1:3721> incr 10
(integer) 11
127.0.0.1:3721> count
(integer) 11
127.0.0.1:3721> info
# Server
rdkit_version:0.0.1
multiplexing_api:select
process_id:15083
tcp_port:3721
uptime_in_seconds:268
uptime_in_days:0
hz:10
# Clients
connected_clients:1
connected_clients_peak:1
# Memory
used_memory_rss:31.89M
used_memory_peak:31.89M
# Counter
counter_version:0.0.1
count:11
last_tick:2015-05-27 20:15:38 +0800
# Stats
total_connections_received:1
total_commands_processed:6
127.0.0.1:3721> xx
(error) ERR unknown command'xx'

Hint: if you are adventurous, try info all

Benchmarking with redis-benchmark

$ redis-benchmark -p 3721 incr
====== count ======
10000 requests completed in 0.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
2.27% <= 2 milliseconds
42.31% <= 3 milliseconds
63.99% <= 4 milliseconds
96.14% <= 5 milliseconds
...
99.97% <= 68 milliseconds
99.98% <= 71 milliseconds
99.99% <= 74 milliseconds
100.00% <= 77 milliseconds
13679.89 requests per second

Since it is single-threaded, the count will be correct:

127.0.0.1:3721> count
(integer) 10000

Implementing blocked commands

Some commands will be blocking: they may either depend on external services or need some background tasks to be run.

The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.

Therefore we introduce Server#blocking methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.

Example: see examples/blocking folder.

# blocking/command_runner.rbmoduleBlockingclassCommandRunner < RDKit::RESPRunnerattr_reader:coredefinitialize(core)@core=coreenddefblock_with_callbackcore.block_with_callback# this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned'OK'enddefblockcore.block'OK'enddefnonblockcore.nonblock'OK'endendend# blocking/core.rbmoduleBlockingclassCore < RDKit::Coredefblock_with_callbackon_success=lambda{'success'}server.blocking(on_success){do_something}enddefblockserver.blocking{do_something}enddefnonblockdo_somethingenddefdo_somethingsleep1enddeftick!endendend

Running:

$ redis-cli -p 3721
127.0.0.1:3721> block
OK
(1.03s)
127.0.0.1:3721> nonblock
OK
(1.01s)
127.0.0.1:3721> block_with_callback
"success"
(1.02s)

Benchmarking:

$ redis-benchmark -p 3721 -n 10 block
====== block ======
10 requests completed in 1.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1027 milliseconds
100.00% <= 1027 milliseconds
9.73 requests per second
$ redis-benchmark -p 3721 -n 10 nonblock
====== nonblock ======
10 requests completed in 10.04 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1001 milliseconds
20.00% <= 2005 milliseconds
30.00% <= 3010 milliseconds
40.00% <= 4013 milliseconds
50.00% <= 5018 milliseconds
60.00% <= 6022 milliseconds
70.00% <= 7027 milliseconds
80.00% <= 8030 milliseconds
90.00% <= 9034 milliseconds
100.00% <= 10039 milliseconds
1.00 requests per second

See the difference between blocking and non-blocking commands?

Additional IO Handler Injection

Since RDKit version 0.1.5, it allows injection of additional IO handlers into the main loop.

For examples, please refer to examples/ioinject for an injected UDP echo server.

Implemented Redis Commands

commandsupportnote
infofulladditional objspace and gc commands
pingfull
echofull
timefull
selectpartial/compatibleredis-benchmark requires select command
configget, set, resetstat
slowlogfull
clientgetname, setname, list, killkill filter only supports id, addr
monitorfull
debugsleep, segfault
shutdownfull
getfull
setwithout options
delfull
keyswithout pattern (return all)
lpushfull
lpopfull
rpopfull
llenfull
lrangepartial (not fully tested)
existsfull
flushdbfull
flushallfull
mgetfull
msetfull
strlenfull
saddfull
scardfull
smembersfull
sismemberfull
sremfull
hsetfull
hgetfull
hexistsfull
hlenfull
hstrlenfull
hdelfull
hkeysfull
hvalsfull
setnxfull
getsetfull

Implemented Additional Commands

commanddescription
gcstart garbage collection immediately
heapdumpObjectSpace.dump_all to ./tmp

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

Contributing

  1. Fork it ( https://github.com/forresty/rdkit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

RDKit

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

The server speaks Redis RESP protocol, so you can reuse many Redis-compatible clients and tools such as:

  • redis-cli
  • redis-benchmark
  • Redic

And a lot more.

RDKit is used to power:

Code ClimateBuild Status

RDKit should work without problem on MRI 2.2+, may encounter bugs on earlier version of MRI or JRuby or Rubinus, in that case, please kindly open an issue on GitHub

Installation

Add this line to your application's Gemfile:

gem'rdkit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rdkit

Usage

Generally, you should implement one subclass for each of the 3 classes: RDKit::RESPResponder, RDKit::Core and RDKit::Server, and spawn one object for each class.

Your server object should have two instance variables @responder and @core pointed to your spawned instances.

RDKit::Server

classYourServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)@core=YourCore.new@responder=YourResponder.new(core)endendserver=YourServer.newtrap(:INT){server.stop}server.start

This will start a TCPServer on 0.0.0.0:3721 and stops when you CTRL-C.

RDKit::RESPResponder

@responder maps Redis commands to its methods and arguments, for example info will be sent to RESPResponder#info, and info all to RESPResponder#info with "all" as its first argument.

The return ruby object of each method will be marshaled as RESP strings, for example 'OK' becomes "+OK\r\n".

For example, with following implementation in your RESPResponder subclass:

defadd(a,b)a.to_i + b.to_iend

You implemented an adder using RDKit! See it in action:

$ redis-cli -p 3721
127.0.0.1:3721> add 1 2
(integer) 3
127.0.0.1:3721> add 5
(error) ERR wrong number of arguments for'add'command
127.0.0.1:3721>

The detailed algorithm can be found in resp.rb, at the time of writing it is like this:

defcompose(data)casedatawhen *%w{OKstringlistsethashzsetnone}"+#{data}\r\n"whentrue":1\r\n"whenfalse":0\r\n"whenInteger":#{data}\r\n"whenArray"*#{data.size}\r\n" + data.map{ |i| compose(i)}.joinwhenNilClass# Null Bulk String, not Null Array of "*-1\r\n""$-1\r\n"whenWrongTypeError"-WRONGTYPE #{data.message}\r\n"whenStandardError"-ERR #{data.message}\r\n"else# always Bulk String"$#{data.bytesize}\r\n#{data}\r\n"endend

RDKit::Core

You are required to implement a tick! method. RDKit will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:

deftick!save_non_critical_data!ifserver.cycles % 1000 == 0end

Examples

See examples under example folder.

Implementing a counter server

A simple counter server source code listing:

require'rdkit'# counter/version.rbmoduleCounterVERSION='0.0.1'end# counter/core.rbmoduleCounterclassCore < RDKit::Coreattr_accessor:countdefinitialize@count=0@last_tick=Time.nowend# `tick!` is called periodically by RDKitdeftick!@last_tick=Time.nowenddefincr(n)@count += nenddefintrospection{counter_version: Counter::VERSION,count: @count,last_tick: @last_tick}endendend# counter/command_runner.rbmoduleCounterclassCommandRunner < RDKit::RESPRunnerdefinitialize(counter)@counter=counterend# every public method of this class will be accessible by clientsdefcount@counter.countenddefincr(n=1)@counter.incr(n.to_i)endendend# counter/server.rbmoduleCounterclassServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)# @core is required by RDKit@core=Core.new# @runner is also required by RDKit@runner=CommandRunner.new(@core)enddefintrospectionsuper.merge(counter: @core.introspection)endendend# start serverserver=Counter::Server.newtrap(:INT){server.stop}server.start

Connect using redis-cli

$ redis-cli -p 3721
127.0.0.1:3721> count
(integer) 0
127.0.0.1:3721> incr
(integer) 1
127.0.0.1:3721> incr 10
(integer) 11
127.0.0.1:3721> count
(integer) 11
127.0.0.1:3721> info
# Server
rdkit_version:0.0.1
multiplexing_api:select
process_id:15083
tcp_port:3721
uptime_in_seconds:268
uptime_in_days:0
hz:10
# Clients
connected_clients:1
connected_clients_peak:1
# Memory
used_memory_rss:31.89M
used_memory_peak:31.89M
# Counter
counter_version:0.0.1
count:11
last_tick:2015-05-27 20:15:38 +0800
# Stats
total_connections_received:1
total_commands_processed:6
127.0.0.1:3721> xx
(error) ERR unknown command'xx'

Hint: if you are adventurous, try info all

Benchmarking with redis-benchmark

$ redis-benchmark -p 3721 incr
====== count ======
10000 requests completed in 0.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
2.27% <= 2 milliseconds
42.31% <= 3 milliseconds
63.99% <= 4 milliseconds
96.14% <= 5 milliseconds
...
99.97% <= 68 milliseconds
99.98% <= 71 milliseconds
99.99% <= 74 milliseconds
100.00% <= 77 milliseconds
13679.89 requests per second

Since it is single-threaded, the count will be correct:

127.0.0.1:3721> count
(integer) 10000

Implementing blocked commands

Some commands will be blocking: they may either depend on external services or need some background tasks to be run.

The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.

Therefore we introduce Server#blocking methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.

Example: see examples/blocking folder.

# blocking/command_runner.rbmoduleBlockingclassCommandRunner < RDKit::RESPRunnerattr_reader:coredefinitialize(core)@core=coreenddefblock_with_callbackcore.block_with_callback# this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned'OK'enddefblockcore.block'OK'enddefnonblockcore.nonblock'OK'endendend# blocking/core.rbmoduleBlockingclassCore < RDKit::Coredefblock_with_callbackon_success=lambda{'success'}server.blocking(on_success){do_something}enddefblockserver.blocking{do_something}enddefnonblockdo_somethingenddefdo_somethingsleep1enddeftick!endendend

Running:

$ redis-cli -p 3721
127.0.0.1:3721> block
OK
(1.03s)
127.0.0.1:3721> nonblock
OK
(1.01s)
127.0.0.1:3721> block_with_callback
"success"
(1.02s)

Benchmarking:

$ redis-benchmark -p 3721 -n 10 block
====== block ======
10 requests completed in 1.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1027 milliseconds
100.00% <= 1027 milliseconds
9.73 requests per second
$ redis-benchmark -p 3721 -n 10 nonblock
====== nonblock ======
10 requests completed in 10.04 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1001 milliseconds
20.00% <= 2005 milliseconds
30.00% <= 3010 milliseconds
40.00% <= 4013 milliseconds
50.00% <= 5018 milliseconds
60.00% <= 6022 milliseconds
70.00% <= 7027 milliseconds
80.00% <= 8030 milliseconds
90.00% <= 9034 milliseconds
100.00% <= 10039 milliseconds
1.00 requests per second

See the difference between blocking and non-blocking commands?

Additional IO Handler Injection

Since RDKit version 0.1.5, it allows injection of additional IO handlers into the main loop.

For examples, please refer to examples/ioinject for an injected UDP echo server.

Implemented Redis Commands

commandsupportnote
infofulladditional objspace and gc commands
pingfull
echofull
timefull
selectpartial/compatibleredis-benchmark requires select command
configget, set, resetstat
slowlogfull
clientgetname, setname, list, killkill filter only supports id, addr
monitorfull
debugsleep, segfault
shutdownfull
getfull
setwithout options
delfull
keyswithout pattern (return all)
lpushfull
lpopfull
rpopfull
llenfull
lrangepartial (not fully tested)
existsfull
flushdbfull
flushallfull
mgetfull
msetfull
strlenfull
saddfull
scardfull
smembersfull
sismemberfull
sremfull
hsetfull
hgetfull
hexistsfull
hlenfull
hstrlenfull
hdelfull
hkeysfull
hvalsfull
setnxfull
getsetfull

Implemented Additional Commands

commanddescription
gcstart garbage collection immediately
heapdumpObjectSpace.dump_all to ./tmp

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

Contributing

  1. Fork it ( https://github.com/forresty/rdkit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

RDKit

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

The server speaks Redis RESP protocol, so you can reuse many Redis-compatible clients and tools such as:

  • redis-cli
  • redis-benchmark
  • Redic

And a lot more.

RDKit is used to power:

Code ClimateBuild Status

RDKit should work without problem on MRI 2.2+, may encounter bugs on earlier version of MRI or JRuby or Rubinus, in that case, please kindly open an issue on GitHub

Installation

Add this line to your application's Gemfile:

gem'rdkit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rdkit

Usage

Generally, you should implement one subclass for each of the 3 classes: RDKit::RESPResponder, RDKit::Core and RDKit::Server, and spawn one object for each class.

Your server object should have two instance variables @responder and @core pointed to your spawned instances.

RDKit::Server

classYourServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)@core=YourCore.new@responder=YourResponder.new(core)endendserver=YourServer.newtrap(:INT){server.stop}server.start

This will start a TCPServer on 0.0.0.0:3721 and stops when you CTRL-C.

RDKit::RESPResponder

@responder maps Redis commands to its methods and arguments, for example info will be sent to RESPResponder#info, and info all to RESPResponder#info with "all" as its first argument.

The return ruby object of each method will be marshaled as RESP strings, for example 'OK' becomes "+OK\r\n".

For example, with following implementation in your RESPResponder subclass:

defadd(a,b)a.to_i + b.to_iend

You implemented an adder using RDKit! See it in action:

$ redis-cli -p 3721
127.0.0.1:3721> add 1 2
(integer) 3
127.0.0.1:3721> add 5
(error) ERR wrong number of arguments for'add'command
127.0.0.1:3721>

The detailed algorithm can be found in resp.rb, at the time of writing it is like this:

defcompose(data)casedatawhen *%w{OKstringlistsethashzsetnone}"+#{data}\r\n"whentrue":1\r\n"whenfalse":0\r\n"whenInteger":#{data}\r\n"whenArray"*#{data.size}\r\n" + data.map{ |i| compose(i)}.joinwhenNilClass# Null Bulk String, not Null Array of "*-1\r\n""$-1\r\n"whenWrongTypeError"-WRONGTYPE #{data.message}\r\n"whenStandardError"-ERR #{data.message}\r\n"else# always Bulk String"$#{data.bytesize}\r\n#{data}\r\n"endend

RDKit::Core

You are required to implement a tick! method. RDKit will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:

deftick!save_non_critical_data!ifserver.cycles % 1000 == 0end

Examples

See examples under example folder.

Implementing a counter server

A simple counter server source code listing:

require'rdkit'# counter/version.rbmoduleCounterVERSION='0.0.1'end# counter/core.rbmoduleCounterclassCore < RDKit::Coreattr_accessor:countdefinitialize@count=0@last_tick=Time.nowend# `tick!` is called periodically by RDKitdeftick!@last_tick=Time.nowenddefincr(n)@count += nenddefintrospection{counter_version: Counter::VERSION,count: @count,last_tick: @last_tick}endendend# counter/command_runner.rbmoduleCounterclassCommandRunner < RDKit::RESPRunnerdefinitialize(counter)@counter=counterend# every public method of this class will be accessible by clientsdefcount@counter.countenddefincr(n=1)@counter.incr(n.to_i)endendend# counter/server.rbmoduleCounterclassServer < RDKit::Serverdefinitializesuper('0.0.0.0',3721)# @core is required by RDKit@core=Core.new# @runner is also required by RDKit@runner=CommandRunner.new(@core)enddefintrospectionsuper.merge(counter: @core.introspection)endendend# start serverserver=Counter::Server.newtrap(:INT){server.stop}server.start

Connect using redis-cli

$ redis-cli -p 3721
127.0.0.1:3721> count
(integer) 0
127.0.0.1:3721> incr
(integer) 1
127.0.0.1:3721> incr 10
(integer) 11
127.0.0.1:3721> count
(integer) 11
127.0.0.1:3721> info
# Server
rdkit_version:0.0.1
multiplexing_api:select
process_id:15083
tcp_port:3721
uptime_in_seconds:268
uptime_in_days:0
hz:10
# Clients
connected_clients:1
connected_clients_peak:1
# Memory
used_memory_rss:31.89M
used_memory_peak:31.89M
# Counter
counter_version:0.0.1
count:11
last_tick:2015-05-27 20:15:38 +0800
# Stats
total_connections_received:1
total_commands_processed:6
127.0.0.1:3721> xx
(error) ERR unknown command'xx'

Hint: if you are adventurous, try info all

Benchmarking with redis-benchmark

$ redis-benchmark -p 3721 incr
====== count ======
10000 requests completed in 0.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.01% <= 1 milliseconds
2.27% <= 2 milliseconds
42.31% <= 3 milliseconds
63.99% <= 4 milliseconds
96.14% <= 5 milliseconds
...
99.97% <= 68 milliseconds
99.98% <= 71 milliseconds
99.99% <= 74 milliseconds
100.00% <= 77 milliseconds
13679.89 requests per second

Since it is single-threaded, the count will be correct:

127.0.0.1:3721> count
(integer) 10000

Implementing blocked commands

Some commands will be blocking: they may either depend on external services or need some background tasks to be run.

The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.

Therefore we introduce Server#blocking methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.

Example: see examples/blocking folder.

# blocking/command_runner.rbmoduleBlockingclassCommandRunner < RDKit::RESPRunnerattr_reader:coredefinitialize(core)@core=coreenddefblock_with_callbackcore.block_with_callback# this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned'OK'enddefblockcore.block'OK'enddefnonblockcore.nonblock'OK'endendend# blocking/core.rbmoduleBlockingclassCore < RDKit::Coredefblock_with_callbackon_success=lambda{'success'}server.blocking(on_success){do_something}enddefblockserver.blocking{do_something}enddefnonblockdo_somethingenddefdo_somethingsleep1enddeftick!endendend

Running:

$ redis-cli -p 3721
127.0.0.1:3721> block
OK
(1.03s)
127.0.0.1:3721> nonblock
OK
(1.01s)
127.0.0.1:3721> block_with_callback
"success"
(1.02s)

Benchmarking:

$ redis-benchmark -p 3721 -n 10 block
====== block ======
10 requests completed in 1.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1027 milliseconds
100.00% <= 1027 milliseconds
9.73 requests per second
$ redis-benchmark -p 3721 -n 10 nonblock
====== nonblock ======
10 requests completed in 10.04 seconds
50 parallel clients
3 bytes payload
keep alive: 1
10.00% <= 1001 milliseconds
20.00% <= 2005 milliseconds
30.00% <= 3010 milliseconds
40.00% <= 4013 milliseconds
50.00% <= 5018 milliseconds
60.00% <= 6022 milliseconds
70.00% <= 7027 milliseconds
80.00% <= 8030 milliseconds
90.00% <= 9034 milliseconds
100.00% <= 10039 milliseconds
1.00 requests per second

See the difference between blocking and non-blocking commands?

Additional IO Handler Injection

Since RDKit version 0.1.5, it allows injection of additional IO handlers into the main loop.

For examples, please refer to examples/ioinject for an injected UDP echo server.

Implemented Redis Commands

commandsupportnote
infofulladditional objspace and gc commands
pingfull
echofull
timefull
selectpartial/compatibleredis-benchmark requires select command
configget, set, resetstat
slowlogfull
clientgetname, setname, list, killkill filter only supports id, addr
monitorfull
debugsleep, segfault
shutdownfull
getfull
setwithout options
delfull
keyswithout pattern (return all)
lpushfull
lpopfull
rpopfull
llenfull
lrangepartial (not fully tested)
existsfull
flushdbfull
flushallfull
mgetfull
msetfull
strlenfull
saddfull
scardfull
smembersfull
sismemberfull
sremfull
hsetfull
hgetfull
hexistsfull
hlenfull
hstrlenfull
hdelfull
hkeysfull
hvalsfull
setnxfull
getsetfull

Implemented Additional Commands

commanddescription
gcstart garbage collection immediately
heapdumpObjectSpace.dump_all to ./tmp

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

Contributing

  1. Fork it ( https://github.com/forresty/rdkit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages