Moneta: A unified interface for key/value stores¶ ↑
Moneta
provides a standard interface for interacting with various kinds of key/value stores. Moneta
supports the well-known NoSQL and document based stores.
A short overview of the features:
-
Supports a lot of backends with consistent behaviour (See below)
-
Allows a full configuration of the serialization -> compression -> adapter stack using proxies (Similar to Rack middlewares)
-
Configurable serialization via
Moneta::Transformer
proxy (Marshal/JSON/YAML and many more) -
Configurable value compression via
Moneta::Transformer
proxy (Zlib, Snappy, LZMA, …) -
Configurable key transformation via
Moneta::Transformer
proxy
-
-
Expiration for all stores (Added via proxy
Moneta::Expires
if not supported natively) -
Atomic operations
-
Atomic incrementation and decrementation for most stores (Method
#increment
and#decrement
) -
Atomic creation of entries (Method
#create
) -
Shared/distributed database-wide synchronization primitives
Moneta::Mutex
andMoneta::Semaphore
-
-
Includes a simple pure-ruby key/value server (
Moneta::Server
) and client (Moneta::Adapters::Client
) -
Integration with Rails, Rack/Rack-Cache, Sinatra, Padrino and Ramaze.
If you are not yet convinced, you might ask why? What are the goals of the project?
-
Get people started quickly with key/value stores! Therefore all the adapters are included in the gem and you are ready to go. Tilt does the same for template languages.
-
Make it easy to compare different key/value stores and benchmark them
-
To hide a lot of different and maybe complex APIs behind one well-designed and simple
Moneta
API -
Give people a starting point or example code to start working with their favourite key/value store. Feel free to copy code, please mention
Moneta
then :) -
Create a reusable piece of code, since similar things are solved over and over again (Rails brings its own cache stores, and many frameworks do the same…)
Moneta
is tested thoroughly using GitHub Actions.
Getting started¶ ↑
Install Moneta
via Rubygems
<s>~ $ gem install moneta </s>~
or add it to your Gemfile
<s>~ ruby gem ‘moneta’ </s>~
Now you are ready to go:
~~~ ruby require ‘moneta’
Create a simple file store¶ ↑
store = Moneta.new
(:File, dir: ‘moneta’)
Store some entries¶ ↑
store = ‘value’
Read entry¶ ↑
store.key?(‘key’) # returns true store # returns ‘value’
store.close ~~~
Links¶ ↑
-
Source: github.com/moneta-rb/moneta
-
Tests and benchmarks: github.com/moneta-rb/moneta/actions
-
API documentation:
-
Latest Gem: rubydoc.info/gems/moneta/frames
-
GitHub main: rubydoc.info/github/moneta-rb/moneta/main/frames
-
In case you are wondering, Moneta
uses Semantic Versioning since v1.0.0.
Supported backends¶ ↑
Out of the box, it supports the following backends. Use the backend name symbol in the Moneta
constructor (e.g. Moneta.new(:Memory)
).
-
Memory:
-
In-memory store (
:Memory
) -
LRU hash - prefer this over :Memory! (
:LRUHash
) -
LocalMemCache (
:LocalMemCache
) -
Memcached store (
:Memcached
,:MemcachedNative
and:MemcachedDalli
)
-
-
Relational Databases:
-
DataMapper (
:DataMapper
) -
ActiveRecord (
:ActiveRecord
) -
Sequel (
:Sequel
) -
Sqlite3 (
:Sqlite
)
-
-
Filesystem:
-
Key/value databases:
-
Berkeley DB using DBM interface or NDBM (Depends on Ruby environment) (
:DBM
) -
Cassandra (
:Cassandra
) -
Daybreak (
:Daybreak
) -
GDBM (
:GDBM
) -
HBase (
:HBase
) -
LevelDB (
:LevelDB
) -
LMDB (
:LMDB
) -
Redis (
:Redis
) -
Riak (
:Riak
) -
SDBM (
:SDBM
) -
KyotoCabinet (
:KyotoCabinet
) -
TokyoCabinet (
:TokyoCabinet
) -
TokyoTyrant (
:TokyoTyrant
) -
Simple Samba database TDB (
:TDB
)
-
-
Document databases:
-
Moneta
network protocols: -
Other
-
Fog cloud storage which supports Amazon S3, Rackspace, etc. (
:Fog
) -
Storage which doesn’t store anything (
:Null
)
-
Some of the backends are not exactly based on key/value stores, e.g. the relational ones. These are useful if you already use the corresponding backend in your application. You get a key/value store for free then without installing any additional services and you still have the possibility to upgrade to a real key/value store.
Backend feature matrix¶ ↑
NOTE: <a name=“backend-matrix”></a>The backend matrix is much more readable on rubydoc.info than on github. Go there!
Adapter | Required gems | MRI support1 | JRuby support1 | Multi-thread safe2 | Multi-process safe3 | Atomic increment4 | Atomic create5 | Native expires6 | Persistent | Key Traversal | Bulk read7 | Bulk write8 | Description |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Persistent stores | |||||||||||||
Mongo | mongo | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | MongoDB database |
Redis | redis | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Redis database |
ActiveRecord | activerecord | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ActiveRecord ORM |
File | - | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | File store |
LMDB | lmdb | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | Symas Lightning Memory-Mapped Database (LMDB) |
Sequel | sequel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | Sequel ORM |
TokyoTyrant | tokyotyrant or ruby-tokyotyrant | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | ✗ | ✓ | ✗ | TokyoTyrant database |
PStore | - | ✓ | ✗ | ✗ | ✓9 | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | PStore store |
YAML | - | ✓ | ✓ | ✗ | ✓9 | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | YAML store |
Sqlite | sqlite3 | ✓ | ✗ | ? | ✓9 | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | Sqlite3 database |
Daybreak | daybreak | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | Incredibly fast pure-ruby key/value store Daybreak |
DBM | - | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | Berkeley DB using DBM interface or NDBM (Depends on Ruby environment) |
GDBM | ffi-gdbm on JRuby | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | GDBM database |
LevelDB | leveldb | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | LevelDB database |
SDBM | - | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | SDBM database |
TDB | tdb | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | TDB database |
KyotoCabinet | kyotocabinet-ruby or kyotocabinet-ruby-reanimated | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | KyotoCabinet database |
TokyoCabinet | tokyocabinet | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | TokyoCabinet database |
DataMapper | dm-core, dm-migrations | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | DataMapper ORM |
Couch | faraday, multi_json | ✓ | ✓ | ✗ | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | CouchDB database |
HBase | hbaserb | ✗ | ✗ | ? | ✓ | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | HBase database |
Cassandra | cassandra | ✓ | ✓ | ? | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | Cassandra distributed database |
LocalMemCache | localmemcache | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | LocalMemCache database |
Fog | fog | ✓ | ✓ | ? | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | Fog cloud store |
Riak | riak-client | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | Riak database |
Non-persistent stores | |||||||||||||
MemcachedDalli | dalli | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗10 | ✗ | ✓ | ✓ | Memcached database with Dalli library |
Memcached | dalli or memcached | ✓ | ?11 | ?11 | ✓ | ✓ | ✓ | ✓ | ✗10 | ✗ | ?11 | ?11 | Memcached database |
MemcachedNative | memcached | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗10 | ✗ | ✗ | ✗ | Memcached database with native library |
Cookie | - | ✓ | ✓ | ✗ | ✓12 | ✓ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | Cookie in memory store |
LRUHash | - | ✓ | ✓ | ✗ | ✓12 | ✓ | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ | LRU memory store |
Memory | - | ✓ | ✓ | ✗ | ✓12 | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | Memory store |
Null | - | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | No database |
Network clients | |||||||||||||
Client | - | ✓ | ✓ | ✗ | ✓ | ?13 | ?13 | ?13 | ?13 | ?13 | ✗ | ✗ | Moneta client adapter |
RestClient | - | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ?13 | ✗ | ✗ | ✗ | Moneta REST client adapter |
-
Indicates that the adapter is expected to work on this platform. Most adapters will at least work on MRI, but some are curently considered unstable, in which case they are not supported on any platform.
-
Make adapters thread-safe by using
Moneta::Lock
or by passing the optionthreadsafe: true
toMoneta#new
. There is alsoMoneta::Pool
which can be used to share a store between multiple threads if the store is multi-process safe. I recommend to add the option:threadsafe
to ensure thread-safety since for example under JRuby and Rubinius even the basic datastructures are not thread safe due to the lack of a global interpreter lock (GIL). This differs from MRI where some adapters might appear thread safe already but only due to the GIL. -
Share a
Moneta
store between multiple processes usingMoneta::Shared
(See below). -
If a store provides atomic increment it can be used with
Moneta::Semaphore
. You can add weak#increment
support using theMoneta::WeakIncrement
proxy. -
If a store provides atomic creation it can be used with
Moneta::Mutex
. You can add weak#create
support using theMoneta::WeakCreate
proxy. -
Add expiration support by using
Moneta::Expires
or by passing the optionexpires: true
toMoneta#new
. -
This indicates that there is some performance gain when fetching multiple values at once using
#values_at
/#fetch_values
or#slice
. For instance, theMGET
instruction in Redis, or the ability to retrieve several rows in one query in SQL. -
This indicates that there is some performance gain when storing multiple key/value pairs at once using
#merge!
/#update
. -
Sqlite/YAML/PStore are multiprocess safe, but the performance suffers badly since the whole database file must be locked for writing. Use a key/value server if you want multiprocess concurrency!
-
There are some servers which use the memcached protocol but which are persistent (e.g. MemcacheDB, Kai, IronCache, Roma, Flare and Kumofs)
-
This feature is only available if the dalli backend is selected
-
Store is multi-process safe because it is an in-memory store, values are not shared between multiple processes
-
Depends on server
Proxies¶ ↑
In addition it supports proxies (Similar to Rack middlewares) which add additional features to storage backends:
-
Moneta::Proxy
andMoneta::Wrapper
are the proxy base classes. -
Moneta::Cache
combine two stores, one as backend and one as cache (e.g.Moneta::Adapters::File
+Moneta::Adapters::LRUHash
). Add it in the builder usinguse(:Cache) {}
. -
Moneta::Expires
to add expiration support to stores which don’t support it natively. Add it in the builder usinguse :Expires
. -
Moneta::Fallback
use a store as a fallback when exceptions occur (by default the:Null
adapter is used so that an error results in a no-op). Add it to the builder usinguse(:Fallback, rescue: IOError)
-
Moneta::Lock
to make store thread safe. Add it in the builder usinguse :Lock
. -
Moneta::Logger
to log database accesses. Add it in the builder usinguse :Logger
. -
Moneta::Pool
to create a pool of stores as a means of making the store thread safe. Add it in the builder usinguse(:Pool, min: 2, max: 4, ttl: 60, timeout: 5) {}
. -
Moneta::Shared
to share a store between multiple processes. Add it in the builder usinguse(:Shared) {}
. -
Moneta::Stack
to stack multiple stores (Read returns result from first where the key is found, writes go to all stores). Add it in the builder usinguse(:Stack) {}
. -
Moneta::Transformer
transforms keys and values (Marshal, YAML, JSON, Base64, MD5, …). Add it in the builder usinguse :Transformer
. -
Moneta::WeakIncrement
andMoneta::WeakCreate
to add#create
and#increment
support without atomicity (weak) to stores which don’t support it. -
Moneta::WeakEachKey
to add key traversal to stores that don’t support it, with the important caveat that only those keys previously seen by this proxy will be traversed.
Check the YARD documentation for more information and examples.
Serializers and compressors (Moneta::Transformer
)¶ ↑
Supported serializers:
-
BEncode (
:bencode
) -
BERT (
:bert
) -
BSON (
:bson
) -
JSON (
:json
) -
Marshal (
:marshal
) -
MessagePack (
:msgpack
) -
Ox (
:ox
) -
PHP (
:php
) -
TNetStrings (
:tnet
) -
YAML (
:yaml
)
Supported value compressors:
-
Bzip2 (
:bzip2
) -
LZ4 (
:lz4
) -
LZMA (
:lzma
) -
LZO (
:lzo
) -
Snappy (
:snappy
) -
QuickLZ (
:quicklz
) -
Zlib (
:zlib
)
Supported encoders:
-
Base64 (RFC 2045;
:base64
) -
URL-safe Base64 (RFC 4648;
:urlsafe_base64
) -
Url escape (
:escape
) -
Hexadecimal (
:hex
) -
QP (
:qp
) -
UUEncode (
:uuencode
)
Special transformers:
-
Digests (MD5, Shas, CityHash, …)
-
Add prefix to keys (
:prefix
) -
HMAC to verify values (
:hmac
, useful forRack::MonetaCookies
)
Moneta
API¶ ↑
The Moneta
API is purposely extremely similar to the Hash API with a few minor additions. Every method takes also a optional option hash. In order so support an identical API across stores, Moneta
does not support partial matches.
~~~
initialize(options) options differs per-store, and is used to set up the store.¶ ↑
retrieve a key. If the key is not available, return nil.¶ ↑
load(key, options = {}) retrieve a key. If the key is not available, return nil.¶ ↑
fetch(key, options = {}, &block) retrieve a key. If the key is not available, execute the¶ ↑
block and return its return value.
fetch(key, value, options = {}) retrieve a key. If the key is not available, return the value,¶ ↑
[]=(key, value) set a value for a key. If the key is already used, clobber it.¶ ↑
keys set using []= will never expire.
store(key, value, options = {}) same as []=, but you can supply options.¶ ↑
delete(key, options = {}) delete the key from the store and return the current value.¶ ↑
key?(key, options = {}) true if the key exists, false if it does not.¶ ↑
increment(key, amount = 1, options = {}) increment numeric value. This is an atomic operation¶ ↑
which is not supported by all stores. Returns current value.
decrement(key, amount = 1, options = {}) increment numeric value. This is an atomic operation¶ ↑
which is not supported by all stores. Returns current value. This is just syntactic sugar for incrementing with a negative value.
create(key, value, options = {}) create entry. This is an atomic operation which is not supported by all stores.¶ ↑
Returns true if the value was created.
values_at(keys, *options) retrieve multiple keys. Returns an array of equal length to the keys.¶ ↑
Each entry in the array is either the value corresponding to the key in the same position, or nil if the key is not available.
fetch_values(keys, *options, &block) retrieve multiple keys. Return is identical to values_at, except that¶ ↑
when a block is given it will be called once for each key that is not available, and the return value of the block will be used in place of nil in the array.
slice(keys, *options) retrieve multiple keys. Returns an enumerable of key-value pairs,¶ ↑
one for each of the supplied keys that is present in the store.
merge!(pairs, options = {}) set values for multiple keys. “pairs” must be an enumerable of¶ ↑
key-value pairs to be stored. Any existing keys will be clobbered.
merge!(pairs, options = {}, &block) set values for multiple keys. For each existing key, execute the block¶ ↑
passing the key, existing value and new value, and store the return value.
update(pairs, options = {}, &block) same as merge!¶ ↑
each_key return an enumerable which will yield all keys in the store, one at a¶ ↑
time. This method is present if and only if the store supports the :each_key feature.
each_key(&block) yield all keys in the store to the block, one at a time. Again, this¶ ↑
method is present if and only if the store supports the :each_key feature.
clear(options = {}) clear all keys in this store.¶ ↑
close close database connection.¶ ↑
features return array of features, e.g. [:create, :expires, :increment]¶ ↑
supports?(feature) returns true if store supports a given feature¶ ↑
~~~
Creating a Store¶ ↑
There is a simple interface to create a store using Moneta.new
. You will get automatic key and value serialization which is provided by Moneta::Transformer
. This allows you to store arbitrary Ruby objects. You can tune some options when you call Moneta.new
. However for very fine tuning use Moneta.build
.
<s>~ ruby store = Moneta.new
(:Memcached, server: ‘localhost:11211’) store = ‘value’ store = {a: 1, b: 2} store = MarshallableRubyObject.new </s>~
If you want to have control over the proxies, you have to use Moneta.build
:
~~~ ruby store = Moneta.build
do # Adds expires proxy use :Expires
# Transform key using Marshal and Base64 and value using Marshal use :Transformer, key: [:marshal, :base64], value: :marshal
# IMPORTANT: adapter must be defined last for the builder to function properly.
# Memory backend adapter :Memory end ~~~
You can also directly access the underlying adapters if you don’t want to use the Moneta
stack.
~~~ ruby db = Moneta::Adapters::File.new
(dir: ‘directory’) db = {a: 1, b: 2} # This will fail since you can only store Strings
However for Mongo and Couch this works¶ ↑
The hash will be mapped directly to a Mongo/Couch document.¶ ↑
db = Moneta::Adapters::Couch.new
db = {a: 1, b: 2}
db = Moneta::Adapters::Mongo.new
db = {a: 1, b: 2} ~~~
Expiration¶ ↑
The Cassandra, Memcached, Redis and Mongo backends support expiration natively.
~~~ ruby cache = Moneta::Adapters::Memcached.new
Or using the builder…¶ ↑
cache = Moneta.build
do adapter :Memcached end
Expires in 60 seconds¶ ↑
cache.store(key, value, expires: 60)
Never expire¶ ↑
cache.store(key, value, expires: 0) cache.store(key, value, expires: false)
Update expires time if value is found¶ ↑
cache.load(key, expires: 30) cache.key?(key, expires: 30)
Or remove the expiration if found¶ ↑
cache.load(key, expires: false) cache.key?(key, expires: 0) ~~~
You can add the expires feature to other backends using the Moneta::Expires
proxy. But be aware that expired values are not deleted automatically if they are not looked up.
~~~ ruby
Using the :expires option¶ ↑
cache = Moneta.new
(:File, dir: ‘…’, expires: true)
or manually by using the proxy…¶ ↑
cache = Moneta::Expires.new
(Moneta::Adapters::File.new
(dir: ‘…’))
or using the builder…¶ ↑
cache = Moneta.build
do use :Expires adapter :File, dir: ‘…’ end ~~~
Key traversal¶ ↑
Where supported by the store’s backend, it is possible to traverse the keys in the store using the #each_key
method. Support for this can be tested by calling store.supports?(:each_key)
, or checking for the presence of :each_key
in store.features
.
<s>~ ruby store.each_key # returns an Enumerable store.each_key do |key| store.load(key) # read operations are supported within the block store = “x” # behaviour of write operations is undefined end </s>~
Atomic operations¶ ↑
Atomic incrementation and raw access¶ ↑
The stores support the #increment
which allows atomic increments of unsigned integer values. If you increment a non existing value, it will be created. If you increment a non integer value an exception will be raised.
<s>~ ruby store.increment(‘counter’) # returns 1, counter created store.increment(‘counter’) # returns 2 store.increment(‘counter’, -1) # returns 1 store.increment(‘counter’, 13) # returns 14 store.increment(‘counter’, 0) # returns 14 store.decrement(‘counter’) # returns 13 store = ‘Moneta’ store.increment(‘name’) # raises an Exception </s>~
If you want to access the counter value you have to use raw access to the datastore. This is only important if you have a Moneta::Transformer
somewhere in your proxy stack which transforms the values e.g. with Marshal
.
~~~ ruby store.increment(‘counter’) # returns 1, counter created store.load(‘counter’, raw: true) # returns 1
store.store(‘counter’, ‘10’, raw: true) store.increment(‘counter’) # returns 11 ~~~
Fortunately there is a nicer way to do this using some syntactic sugar!
~~~ ruby store.increment(‘counter’) # returns 1, counter created store.raw # returns 1 store.raw.load(‘counter’) # returns 1
store.raw = ‘10’ store.increment(‘counter’) # returns 11 ~~~
You can also keep the raw
store in a variable and use it like this:
~~~ ruby counters = store.raw
counters.increment(‘counter’) # returns 1, counter created counters # returns 1 counters.load(‘counter’) # returns 1
counters = ‘10’ counters.increment(‘counter’) # returns 11 ~~~
Atomic create¶ ↑
The stores support the #create
which allows atomic creation of entries. #create
returns true if the value was created.
<s>~ ruby store.create(‘key’, ‘value’) # returns true store.create(‘key’, ‘other value’) # returns false </s>~
Atomic bulk operations¶ ↑
All stores support storage and retrieval of multiple keys using #values_at
/#fetch_values
/#slice
and #merge!
/#update
. Wherever possible, these operations are performed atomically. When this is not possible, the #load
and #store
methods are called once for each key.
~~~ ruby store.merge!(‘key1’ => ‘value1’, ‘key2’ => ‘value2’) # stores two keys store.values_at(‘key1’, ‘key2’, ‘key3’) # returns [‘value1’, ‘value2’, nil] store.fetch(‘key1’, ‘key3’) { |k| k + ‘ missing’ } # returns [‘key1’, ‘key3 missing’] store.slice(‘key1’, ‘key2’, ‘key3’) # returns enumerable of [‘key1’, ‘value1’], [‘key2’, ‘value2’]
store.merge!(‘key2’ => ‘new value2’, ‘key3’ => ‘value3’) do |key, value, new_value| [value, new_value].join(‘+’) end # stores “value3” and “value2+new value2” ~~~
Shared/distributed synchronization primitives¶ ↑
Moneta
provides shared/distributed synchronization primitives which are shared database-wide between all clients.
Moneta::Mutex
allows a single thread to enter a critical section.
~~~ ruby mutex = Moneta::Mutex.new
(store, ‘mutex_key’)
mutex.synchronize do mutex.locked? # returns true
# Synchronized access to counter store += 1 end
begin mutex.lock mutex.locked? # returns true # … ensure mutex.unlock end ~~~
Moneta::Semaphore
allows max_concurrent
threads to enter a critical section.
~~~ ruby semaphore = Moneta::Semaphore.new
(store, ‘semaphore_counter’, max_concurrent)
semaphore.synchronize do semaphore.locked? # returns true # … end
begin semaphore.enter semaphore.locked? # returns true # … ensure semaphore.leave end ~~~
Weak atomic operations¶ ↑
If an underlying adapter doesn’t provide atomic #create
or #increment
and #decrement
you can use the proxies Moneta::WeakIncrement
and Moneta::WeakCreate
to add support without atomicity.
But then you have to ensure that the store is not shared by multiple processes and thread-safety is provided by Moneta::Lock
.
Syntactic sugar and option merger¶ ↑
For raw data access as described before the class Moneta::OptionMerger
is used. It works like this:
~~~ ruby
All methods after ‘with’ get the options passed¶ ↑
store.with(raw: true).load(‘key’)
You can also specify the methods¶ ↑
store.with(raw: true, only: :load).load(‘key’) store.with(raw: true, except: [:key?, :increment]).load(‘key’)
Syntactic sugar for raw access¶ ↑
store.raw.load(‘key’)
Access substore where all keys get a prefix¶ ↑
substore = store.prefix(‘sub’) substore = ‘value’ store # returns nil store # returns ‘value’
Set expiration time for all keys¶ ↑
short_lived_store = store.expires(60) short_lived_store = ‘value’ ~~~
Add proxies to existing store¶ ↑
You can add proxies to an existing store. This is useful if you want to compress only a few values for example.
~~~ ruby compressed_store = store.with(prefix: ‘compressed’) do use :Transformer, value: :zlib end
store = ‘this value will not be compressed’ compressed_store = ‘value will be compressed’ ~~~
Framework Integration¶ ↑
Inspired by redis-store there exist integration classes for Rails and Rack/Rack-Cache. You can also use all the Rack
middlewares together with Rails and the Sinatra framework. There exist the following integration classes:
-
-
Rack::Session::Moneta
is aRack
middleware to useMoneta
for storing sessions -
Rack::MonetaStore
is aRack
middleware which places aMoneta
store in the environment and enables per-request caching -
Rack::MonetaCookies
is aRack
middleware which usesMoneta
to store cookies -
Rack::MonetaRest
is aRack
application which exposes aMoneta
store via REST/HTTP -
Rack::Cache::Moneta
provides meta and entity stores for Rack-Cache
-
-
-
Ramaze::Cache::Moneta
is integrated into the Ramaze project and allows Ramaze to useMoneta
as caching store
-
-
Padrino adopted
Moneta
to replace their cache stores in padrino-cache.
Rack
¶ ↑
Session store¶ ↑
You can use Moneta
as a Rack session store. Use it in your config.ru
like this:
~~~ ruby require ‘rack/session/moneta’
Use only the adapter name¶ ↑
use Rack::Session::Moneta
, store: :Redis
Use Moneta.new
¶ ↑
use Rack::Session::Moneta
, store: Moneta.new
(:Memory, expires: true)
Set rack options¶ ↑
use Rack::Session::Moneta
, key: ‘rack.session’, domain: ‘foo.com’, path: ‘/’, expire_after: 2592000, store: Moneta.new
(:Memory, expires: true)
Use the Moneta
builder¶ ↑
use Rack::Session::Moneta
do use :Expires adapter :Memory end ~~~
Moneta
middleware¶ ↑
There is a simple middleware which places a Moneta
store in the Rack
environment at env['rack.moneta_store']
. It supports per-request caching if you add the option cache: true
. Use it in your config.ru
like this:
~~~ ruby require ‘rack/moneta_store’
Add Rack::MonetaStore
somewhere in your rack stack¶ ↑
use Rack::MonetaStore
, :Memory, cache: true
run lambda { |env| env # is a Moneta
store with per-request caching }
Pass it a block like the one passed to Moneta.build
¶ ↑
use Rack::MonetaStore
do use :Transformer, value: :zlib adapter :Cookie end
run lambda { |env| env # is a Moneta
store without caching } ~~~
REST server¶ ↑
If you want to expose your Moneta
key/value store via HTTP, you can use the Rack/Moneta REST service. Use it in your config.ru
like this:
~~~ ruby require ‘rack/moneta_rest’
map ‘/moneta’ do run Rack::MonetaRest.new
(:Memory) end
Or pass it a block like the one passed to Moneta.build
¶ ↑
run Rack::MonetaRest.new
do use :Transformer, value: :zlib adapter :Memory end ~~~
Rack-Cache¶ ↑
You can use Moneta
as a Rack-Cache store. Use it in your config.ru
like this:
~~~ ruby require ‘rack/cache/moneta’
use Rack::Cache
, metastore: ‘moneta://Memory?expires=true’, entitystore: ‘moneta://Memory?expires=true’
Or used named Moneta
stores¶ ↑
Rack::Cache::Moneta['named_metastore']
= Moneta.build
do use :Expires adapter :Memory end use Rack::Cache
, metastore: ‘moneta://named_metastore’, entity_store: ‘moneta://named_entitystore’ ~~~
Cookies¶ ↑
Use Moneta
to store cookies in Rack. It uses the Moneta::Adapters::Cookie
. You might wonder what the purpose of this store or Rack
middleware is: It makes it possible to use all the transformers on the cookies (e.g. :prefix
, :marshal
and :hmac
for value verification).
~~~ ruby require ‘rack/moneta_cookies’
use Rack::MonetaCookies
, domain: ‘example.com’, path: ‘/path’ run lambda { |env| req = Rack::Request.new(env) req.cookies #=> is now a Moneta
store! env #=> is now a Moneta
store! req.cookies #=> retrieves ‘key’ req.cookies = ‘value’ #=> sets ‘key’ req.cookies.delete(‘key’) #=> removes ‘key’ [200, {}, []] } ~~~
Rails¶ ↑
Session store¶ ↑
Add the session store in your application configuration config/environments/*.rb
.
~~~ ruby require ‘action_dispatch/middleware/session/moneta_store’
Only by adapter name¶ ↑
config.cache_store :moneta_store, store: :Memory
Use Moneta.new
¶ ↑
config.cache_store :moneta_store, store: Moneta.new
(:Memory)
Use the Moneta
builder¶ ↑
config.cache_store :moneta_store, store: Moneta.build
do use :Expires adapter :Memory end ~~~
Cache store¶ ↑
Add the cache store in your application configuration config/environments/*.rb
. Unfortunately the Moneta
cache store doesn’t support matchers. If you need these features use a different server-specific implementation.
~~~ ruby require ‘active_support/cache/moneta_store’
Only by adapter name¶ ↑
config.cache_store :moneta_store, store: :Memory
Use Moneta.new
¶ ↑
config.cache_store :moneta_store, store: Moneta.new
(:Memory)
Use the Moneta
builder¶ ↑
config.cache_store :moneta_store, store: Moneta.build
do use :Expires adapter :Memory end ~~~
Padrino¶ ↑
Padrino adopted Moneta
to replace their cache stores in padrino-cache. You use it like this
~~~ ruby
Global Padrino caching¶ ↑
Don’t forget the expires: [true, Integer] if you want expiration support!¶ ↑
Padrino.cache = Moneta.new
(:Memory, expires: true)
Application caching¶ ↑
Don’t forget the expires: [true, Integer] if you want expiration support!¶ ↑
set :cache, Moneta.new
(:Memory, expires: true) ~~~
Advanced¶ ↑
Build your own key value server¶ ↑
You can use Moneta
to build your own key/value server which is shared between multiple processes. If you run the following code in two different processes, they will share the same data which will also be persistet in the database shared.db
.
~~~ ruby require ‘moneta’
store = Moneta.build
do use :Transformer, key: :marshal, value: :marshal use :Shared do use :Cache do cache do adapter :LRUHash end backend do adapter :GDBM, file: ‘shared.db’ end end end end ~~~
If you want to go further, you might want to take a look at Moneta::Server
and Moneta::Adapters::Client
which are used by Moneta::Shared
and provide the networking communication. But be aware that they are experimental and subjected to change. They provide an acceptable performance (for being ruby only), but don’t have a stable protocol yet.
You might wonder why I didn’t use DRb to implement server and client - in fact my first versions used it, but with much worse performance and it was real fun to implement the networking directly :) There is still much room for improvement and experiments, try EventMachine, try Kgio, …
ToyStore ORM¶ ↑
If you want something more advanced to handle your objects and relations, use John Nunemaker’s ToyStore which works together with Moneta
. Assuming that Person
is a ToyStore::Object
you can add persistence using Moneta
as follows:
~~~ ruby
Use the Moneta
Redis backend¶ ↑
Person.adapter :memory, Moneta.new
(:Redis) ~~~
Testing and Benchmarks¶ ↑
Testing is done using GitHub Actions. Currently we support MRI Ruby >= 2.4.0 (but not yet 3.x) and the JRuby >= 9.2.9.0. MRI 2.3.0 should mostly still work, but is no longer tested in CI.
<s>Benchmarks for each store are done on Travis-CI for each build.</s> At the time of writing, benchmarks still need to be migrated from Travis to GitHub Actions. Take a look there to compare the speed of the different key value stores for different key/value sizes and size distributions. Feel free to add your own configurations! The impact of Moneta
should be minimal since it is only a thin layer on top of the different stores.
How to contribute?¶ ↑
Always feel free to open an issue on github.com/moneta-rb/moneta/issues if something doesn’t work as you expect it to work. Feedback is also very welcome!
My only request about patches is that you please try to test them before submitting.
Contribute an adapter¶ ↑
If you want support for another adapter you can at first at it to the list of missing adapters at github.com/moneta-rb/moneta/issues/16
If you choose to implement an adapter please also add tests. Please check also if anything in .github/workflows
needs changes, for example if you need to start additional services.
Check if the default settings in Moneta#new are appropriate for your adapter. If not specify a better one.
Don’t forget to edit the README.md and the CHANGES.
Alternatives¶ ↑
-
Horcrux: Used at github, supports batch operations but only Memcached backend
-
ActiveSupport::Cache::Store: The Rails cache store abstraction
-
ToyStore: ORM mapper for key/value stores
-
ToyStore Adapter: Adapter to key/value stores used by ToyStore,
Moneta
can be used directly with the ToyStore Memory adapter -
Cache: Rubygem cache wraps Memcached and Redis
-
Ramaze::Cache: Cache stores of the Ramaze framework with support for LocalMemCache, Memcached, Sequel, Redis, …
Authors¶ ↑
-
Originally by Yehuda Katz and contributors