porpoise 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Dockerfile +15 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +109 -0
- data/Rakefile +2 -0
- data/bin/console +20 -0
- data/bin/setup +8 -0
- data/lib/active_support/cache/porpoise_store.rb +182 -0
- data/lib/generators/porpoise/install_generator.rb +12 -0
- data/lib/generators/porpoise/templates/migration.rb +20 -0
- data/lib/porpoise/hash.rb +129 -0
- data/lib/porpoise/key.rb +115 -0
- data/lib/porpoise/key_value_object.rb +61 -0
- data/lib/porpoise/set.rb +194 -0
- data/lib/porpoise/string.rb +155 -0
- data/lib/porpoise/util.rb +9 -0
- data/lib/porpoise/version.rb +3 -0
- data/lib/porpoise.rb +37 -0
- data/porpoise.gemspec +33 -0
- metadata +206 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 5b24e7c5e270d8c55573ea5d9224562ec9f30bca
|
|
4
|
+
data.tar.gz: 68a6fb51394b3ac03bd84b9d66f49e4350cd7f88
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 87f66069bfeadce307935f5692a920fe845fcd709a8781b0d992dd1a3960e21b5c25ed59ee33c3c4970d6f2e55c100512193de386604a226bc2f1be5b30198da
|
|
7
|
+
data.tar.gz: 896cd60fa409cbda94f9158c69000ae5e11434594a24b35c86ebe6047c5685e389033da7c5d6e5371a08dffb7c7b351edeb69d6683abe29f5194bea70903890e
|
data/.gitignore
ADDED
data/Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Build with docker build -t porpoise-app .
|
|
2
|
+
# Run with docker run -it --rm -v "$PWD:/porpoise" porpoise-app
|
|
3
|
+
|
|
4
|
+
FROM ruby:2.3.3
|
|
5
|
+
RUN apt-get update -qq && apt-get install -y build-essential ruby-dev vim sqlite3
|
|
6
|
+
|
|
7
|
+
ENV APP_HOME /porpoise
|
|
8
|
+
RUN mkdir $APP_HOME
|
|
9
|
+
WORKDIR $APP_HOME
|
|
10
|
+
ADD . $APP_HOME
|
|
11
|
+
|
|
12
|
+
RUN gem install rake -v=10.5.0
|
|
13
|
+
RUN bundle install
|
|
14
|
+
|
|
15
|
+
CMD /bin/bash
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 rubygems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Porpoise
|
|
2
|
+
|
|
3
|
+
## Welcome
|
|
4
|
+
|
|
5
|
+
Porpoise implements a Redis like interface using MySQL as its storage engine. This provides an easy replacement for Rails applications that currently use Redis to store key/value data and would like to switch to MySQL for whatever reason.
|
|
6
|
+
|
|
7
|
+
### Performance
|
|
8
|
+
|
|
9
|
+
Redis outperforms this implementation by a long shot and I don't think I have to tell why. For an SQL based solution it still performs alright though. Besides, this thing integrates with Rails, uses ActiveRecord and all the bloat that comes with it. And altough a cache should help performance, cache read/write speed sometimes are not the bottleneck.
|
|
10
|
+
|
|
11
|
+
### Then why?
|
|
12
|
+
|
|
13
|
+
To simplify your stack? To get a real multi-master setup using MySQL/Galera? To get rid of an unstable situation?
|
|
14
|
+
|
|
15
|
+
This gem was written out of the need to get rid of our shaky Redis Dynomite cluster which we implemented due to the requirement of having real multi-master clusters. Master-slave- and failover setups tend to break over time, so multi-master is the only acceptable way to go.
|
|
16
|
+
|
|
17
|
+
So Redis /w Dynomite kept exploding at every little burp so we needed a solution. MySQL / Galera was already there as our primary datastore so the alternative was easily chosen. Reasons: proven multi-master setup and a more easy stack. With our userbase and use of Redis, performance was of minor importance.
|
|
18
|
+
|
|
19
|
+
## Compatibility
|
|
20
|
+
|
|
21
|
+
This gem has been tested with Rails 3, but probably also works with newer versions of Rails.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Add this line to your application's Gemfile:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
gem 'porpoise'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
And then execute:
|
|
32
|
+
|
|
33
|
+
$ bundle
|
|
34
|
+
|
|
35
|
+
Or install it yourself as:
|
|
36
|
+
|
|
37
|
+
$ gem install porpoise
|
|
38
|
+
|
|
39
|
+
After installation of the gem, install the required migration:
|
|
40
|
+
|
|
41
|
+
$ rails generate porpoise:install
|
|
42
|
+
|
|
43
|
+
Porpoise runs in a different database for easy optimization and decoupling. Add an entry in your config/database.yml for porpoise.
|
|
44
|
+
|
|
45
|
+
porpoise_development:
|
|
46
|
+
adapter: mysql2
|
|
47
|
+
username: <porpoise_db_user>
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
Run migrations to install the required table:
|
|
51
|
+
|
|
52
|
+
rake db:migrate
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Caching in Rails
|
|
57
|
+
|
|
58
|
+
Using Porpoise as caching backend in Rails is easy. Change your cache store config like this:
|
|
59
|
+
|
|
60
|
+
config.cache_store = :porpoise_store
|
|
61
|
+
|
|
62
|
+
That's all! Optionally you can set a caching namespace like this:
|
|
63
|
+
|
|
64
|
+
config.cache_store = :porpoise_store, { namespace: :your_namespace }
|
|
65
|
+
|
|
66
|
+
Namespacing automatically preprends each cache key with the chosen namespace.
|
|
67
|
+
|
|
68
|
+
### As generic key/value storage
|
|
69
|
+
|
|
70
|
+
Porpoise was designed as an easy replacement for Redis. Therefore it implements various of the commonly used types Redis knows (strings, sets and hashes) and most of their functions. Access them like this:
|
|
71
|
+
|
|
72
|
+
Porpoise::String.<redis-function-name-and-arguments>
|
|
73
|
+
Porpoise::Set.<redis-function-name-and-arguments>
|
|
74
|
+
Porpoise::Hash.<redis-function-name-and-arguments>
|
|
75
|
+
|
|
76
|
+
Namespacing is easy as well. Surround your Porpoise calls in a block like this example:
|
|
77
|
+
|
|
78
|
+
Porpoise.with_namespace(:your_namespace) do
|
|
79
|
+
Porpoise::String.set('test-key', 'test-value')
|
|
80
|
+
Porpoise::Hash.hset('test-key', 'foo', 'bar')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
Fork this repo. Development is done from within a Docker container for which the Dockerfile is included in this repo. Build and start the the container to access this gems development environment:
|
|
86
|
+
|
|
87
|
+
docker build -t porpoise-app .
|
|
88
|
+
docker run -it --rm -v "$PWD:/porpoise" porpoise-app
|
|
89
|
+
|
|
90
|
+
Run all other actions from within the container. Tests:
|
|
91
|
+
|
|
92
|
+
bundle exec rspec spec
|
|
93
|
+
|
|
94
|
+
Tests take some time as there's some performance tests included.
|
|
95
|
+
|
|
96
|
+
Console:
|
|
97
|
+
|
|
98
|
+
./bin/console
|
|
99
|
+
|
|
100
|
+
PR's are welcome :)
|
|
101
|
+
|
|
102
|
+
## Todo
|
|
103
|
+
|
|
104
|
+
Of course there's still some work to do. Performance tests do not represent actual performance, as they currently use a memory store which should be file based to include disk I/O in the tests.
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sentialabs/porpoise.
|
|
109
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'active_record'
|
|
4
|
+
|
|
5
|
+
ENV['rack_env'] = 'test'
|
|
6
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
|
7
|
+
load File.join('spec/support/schema.rb')
|
|
8
|
+
|
|
9
|
+
require "bundler/setup"
|
|
10
|
+
require "porpoise"
|
|
11
|
+
|
|
12
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
13
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
14
|
+
|
|
15
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
16
|
+
# require "pry"
|
|
17
|
+
# Pry.start
|
|
18
|
+
|
|
19
|
+
require "irb"
|
|
20
|
+
IRB.start
|
data/bin/setup
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
module ActiveSupport
|
|
2
|
+
module Cache
|
|
3
|
+
class PorpoiseStore < Store
|
|
4
|
+
# The maximum number of objects to store in
|
|
5
|
+
# the short life cache.
|
|
6
|
+
SHORT_LIFE_CACHE_SIZE = 5000
|
|
7
|
+
|
|
8
|
+
# The number of seconds items in the short
|
|
9
|
+
# cache are allowed to live.
|
|
10
|
+
SHORT_LIFE_CACHE_TIME = 5
|
|
11
|
+
|
|
12
|
+
attr_reader :namespace
|
|
13
|
+
attr_reader :slc, :slt # short life cache
|
|
14
|
+
|
|
15
|
+
def initialize(options = {})
|
|
16
|
+
@namespace = options.fetch(:namespace, "active-support-cache").to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cleanup(options = nil)
|
|
20
|
+
short_mem_reset
|
|
21
|
+
Porpoise::KeyValueObject.where(["`key` LIKE ?", "#{@namespace}:%"]).
|
|
22
|
+
where(["expiration_date IS NOT NULL AND expiration_date < ?", Time.now]).
|
|
23
|
+
delete_all
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clear(options = nil)
|
|
29
|
+
short_mem_reset
|
|
30
|
+
Porpoise::KeyValueObject.where(["`key` LIKE ?", "#{@namespace}:%"]).delete_all
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def decrement(name, amount, options = nil)
|
|
34
|
+
Porpoise.with_namespace(@namespace) {
|
|
35
|
+
v = read(name)
|
|
36
|
+
v = v.to_i - amount.to_i
|
|
37
|
+
write(name, v, options)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(name, options = nil)
|
|
42
|
+
Porpoise.with_namespace(@namespace) {
|
|
43
|
+
short_mem_del(name)
|
|
44
|
+
Porpoise::Key.del(name)
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def delete_matched(matcher, options = nil)
|
|
49
|
+
short_mem_reset
|
|
50
|
+
Porpoise.with_namespace(@namespace) { Porpoise::Key.del_matched(matcher) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def exists?(name, options = nil)
|
|
54
|
+
Porpoise.with_namespace(@namespace) { Porpoise::Key.exists(name) == 1 }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fetch(name, options = nil)
|
|
58
|
+
res = read(name)
|
|
59
|
+
if res.nil? && block_given?
|
|
60
|
+
res = yield(name)
|
|
61
|
+
write(name, res, options)
|
|
62
|
+
end
|
|
63
|
+
return res
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def fetch_multi(*names)
|
|
67
|
+
res = read_multi(*names)
|
|
68
|
+
return res unless block_given?
|
|
69
|
+
|
|
70
|
+
mres = {}
|
|
71
|
+
res.each do |name, value|
|
|
72
|
+
if value.nil?
|
|
73
|
+
mres[name] = yield(name)
|
|
74
|
+
write(name, mres[name])
|
|
75
|
+
else
|
|
76
|
+
mres[name] = value
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return mres
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def increment(name, amount, options = nil)
|
|
84
|
+
Porpoise.with_namespace(@namespace) {
|
|
85
|
+
v = read(name)
|
|
86
|
+
v = v.to_i + amount.to_i
|
|
87
|
+
write(name, v, options)
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read(name, options = nil)
|
|
92
|
+
val = Porpoise.with_namespace(@namespace) {
|
|
93
|
+
short_mem_read(name) { Porpoise::String.get(name) }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
return val.nil? ? nil : Marshal.load(val)
|
|
98
|
+
rescue TypeError
|
|
99
|
+
return val
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def read_multi(*names)
|
|
104
|
+
result = {}
|
|
105
|
+
names.each do |name|
|
|
106
|
+
val = Porpoise.with_namespace(@namespace) {
|
|
107
|
+
short_mem_read(name) { Porpoise::String.get(name) }
|
|
108
|
+
}
|
|
109
|
+
begin
|
|
110
|
+
result[name] = (val.nil? ? nil : Marshal.load(val))
|
|
111
|
+
rescue TypeError
|
|
112
|
+
result[name] = val
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
return result
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def write(name, value, options = nil)
|
|
119
|
+
options = {} if options.nil?
|
|
120
|
+
Porpoise.with_namespace(@namespace) {
|
|
121
|
+
short_mem_write(name, value, options) {
|
|
122
|
+
Porpoise::String.set(name, Marshal.dump(value), options.fetch(:expires_in, nil))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def short_mem_reset
|
|
130
|
+
@slc = {}
|
|
131
|
+
@slt = {}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def short_mem_write(name, value, options = nil)
|
|
135
|
+
@slc ||= {}
|
|
136
|
+
@slt ||= {}
|
|
137
|
+
|
|
138
|
+
# Do not write the short life cache if an item has an expiration time
|
|
139
|
+
unless options && options.has_key?(:expires_in)
|
|
140
|
+
@slt[name] = Time.now.to_i
|
|
141
|
+
@slc[name] = value
|
|
142
|
+
|
|
143
|
+
# Remove the oldest entries when cache gets to big
|
|
144
|
+
if @slt.keys.size >= SHORT_LIFE_CACHE_SIZE
|
|
145
|
+
kk = @slt.sort_by { |k,v| value }.shift(@slt.keys.size - SHORT_LIFE_CACHE_SIZE).map { |kv| kv[0] }
|
|
146
|
+
kk.each do |k|
|
|
147
|
+
@slt.delete(k)
|
|
148
|
+
@slc.delete(k)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
yield if block_given?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def short_mem_read(name)
|
|
157
|
+
@slc ||= {}
|
|
158
|
+
@slt ||= {}
|
|
159
|
+
v = @slc.fetch(name, nil)
|
|
160
|
+
|
|
161
|
+
# Remove dead items
|
|
162
|
+
if v && (@slt[name] + SHORT_LIFE_CACHE_TIME) < Time.now.to_i
|
|
163
|
+
@slc.delete(name)
|
|
164
|
+
@slt.delete(name)
|
|
165
|
+
v = nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if v.nil? && block_given?
|
|
169
|
+
v = yield
|
|
170
|
+
short_mem_write(name, v) unless v.nil?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
return v
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def short_mem_del(name)
|
|
177
|
+
@slc.delete(name)
|
|
178
|
+
@slt.delete(name)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
|
|
3
|
+
module Porpoise
|
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
|
5
|
+
source_root File.expand_path("../templates", __FILE__)
|
|
6
|
+
|
|
7
|
+
desc "Install database migration file"
|
|
8
|
+
def create_migration_file
|
|
9
|
+
copy_file "migration.rb", "db/migrate/#{Date.today.strftime('%Y%m%d%H%M%S')}_porpoise_create_key_value_objects.rb"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class PorpoiseCreateKeyValueObjects < ActiveRecord::Migration
|
|
2
|
+
def up
|
|
3
|
+
Porpoise::KeyValueObject.connection.create_table :key_value_objects, id: false } do |t|
|
|
4
|
+
t.string :key, null: false
|
|
5
|
+
t.string :data_type, null: false, limit: 10
|
|
6
|
+
t.text :value, limit: 65000000, null: false
|
|
7
|
+
t.datetime :expiration_date
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Porpoise::KeyValueObject.connection.add_index :key_value_objects, :key, unique: true
|
|
11
|
+
Porpoise::KeyValueObject.connection.add_index :key_value_objects, [:key, :expiration_date]
|
|
12
|
+
Porpoise::KeyValueObject.connection.add_index :key_value_objects, :key, name: 'key_fulltext_idx', type: :fulltext
|
|
13
|
+
Porpoise::KeyValueObject.connection.add_index :key_value_objects, :data_type
|
|
14
|
+
Porpoise::KeyValueObject.connection.add_index :key_value_objects, :expiration_date
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def down
|
|
18
|
+
Porpoise::KeyValueObject.connection.drop_table :key_value_objects
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module Porpoise
|
|
2
|
+
module Hash
|
|
3
|
+
class << self
|
|
4
|
+
def hdel(key, *fields)
|
|
5
|
+
o = find_stored_object(key)
|
|
6
|
+
current_keys = o.value.keys.size
|
|
7
|
+
o.value = o.value.delete_if { |k,v| fields.include?(k) }
|
|
8
|
+
o.save
|
|
9
|
+
|
|
10
|
+
return current_keys - o.value.keys.size
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def hexists(key, field)
|
|
14
|
+
o = find_stored_object(key)
|
|
15
|
+
o.value.has_key?(field) ? 1 : 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def hget(key, field)
|
|
19
|
+
o = find_stored_object(key)
|
|
20
|
+
o.value.fetch(field, nil)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def hgetall(key)
|
|
24
|
+
o = find_stored_object(key)
|
|
25
|
+
o.value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def hincrby(key, field, increment)
|
|
29
|
+
o = find_stored_object(key)
|
|
30
|
+
o.value[field] += increment.to_i
|
|
31
|
+
o.save
|
|
32
|
+
o.value[field]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def hincrbyfloat(key, field, increment)
|
|
36
|
+
o = find_stored_object(key)
|
|
37
|
+
o.value[field] = (o.value[field] + increment.to_f).round(5)
|
|
38
|
+
o.save
|
|
39
|
+
o.value[field]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def hkeys(key)
|
|
43
|
+
o = find_stored_object(key)
|
|
44
|
+
o.value.keys
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def hlen(key)
|
|
48
|
+
o = find_stored_object(key)
|
|
49
|
+
o.value.keys.size
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def hmget(key, *fields)
|
|
53
|
+
o = find_stored_object(key)
|
|
54
|
+
fields.map { |f| o.value.fetch(f, nil) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def hmset(key, *fields_and_values)
|
|
58
|
+
o = find_stored_object(key)
|
|
59
|
+
set_values = ::Hash[*fields_and_values]
|
|
60
|
+
|
|
61
|
+
set_values.keys.each do |k|
|
|
62
|
+
o.value[k] = set_values[k]
|
|
63
|
+
end
|
|
64
|
+
o.save
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def hset(key, field, value)
|
|
68
|
+
o = find_stored_object(key)
|
|
69
|
+
current_value = o.value.fetch(field, "")
|
|
70
|
+
o.value[field] = value
|
|
71
|
+
|
|
72
|
+
if o.save
|
|
73
|
+
return current_value == value ? 0 : 1
|
|
74
|
+
else
|
|
75
|
+
return 0
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def hsetnx(key, field, value)
|
|
80
|
+
o = find_stored_object(key)
|
|
81
|
+
ahk = o.value.has_key?(field)
|
|
82
|
+
o.value[field] = value unless ahk
|
|
83
|
+
|
|
84
|
+
if o.save
|
|
85
|
+
return ahk ? 0 : 1
|
|
86
|
+
else
|
|
87
|
+
return 0
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def hstrlen(key, field)
|
|
92
|
+
o = find_stored_object(key)
|
|
93
|
+
o.value.fetch(field, "").to_s.size
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def hvals(key)
|
|
97
|
+
o = find_stored_object(key)
|
|
98
|
+
o.value.values
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def find_stored_object(key,
|
|
104
|
+
raise_on_type_mismatch = true,
|
|
105
|
+
raise_on_not_found = false)
|
|
106
|
+
|
|
107
|
+
key = Porpoise::key_with_namespace(key)
|
|
108
|
+
o = Porpoise::KeyValueObject.not_expired.where(key: key).first
|
|
109
|
+
|
|
110
|
+
if raise_on_type_mismatch && !o.nil? && o.data_type != 'Hash'
|
|
111
|
+
raise Porpoise::TypeMismatch.new(
|
|
112
|
+
"Key #{key} is not of type Hash (is #{o.data_type})"
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if raise_on_not_found && o.nil?
|
|
117
|
+
raise Porpoise::KeyNotFound.new("Key #{key} could not be found")
|
|
118
|
+
elsif o.nil?
|
|
119
|
+
o = Porpoise::KeyValueObject.new(key: key, value: ::Hash.new)
|
|
120
|
+
elsif o.expired?
|
|
121
|
+
o.delete
|
|
122
|
+
o = Porpoise::KeyValueObject.new(key: key, value: ::Hash.new)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
return o
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/porpoise/key.rb
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module Porpoise
|
|
2
|
+
module Key
|
|
3
|
+
class << self
|
|
4
|
+
def del(key, *other_keys)
|
|
5
|
+
o = find_stored_object(key)
|
|
6
|
+
aff = 0
|
|
7
|
+
|
|
8
|
+
unless o.nil?
|
|
9
|
+
o.delete
|
|
10
|
+
aff += 1
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if other_keys.any?
|
|
14
|
+
aff += Porpoise::KeyValueObject.not_expired.
|
|
15
|
+
where(key: other_keys.map { |k| Porpoise::key_with_namespace(k) }).
|
|
16
|
+
delete_all
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
return aff
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def del_matched(matcher)
|
|
23
|
+
matcher = Porpoise::key_with_namespace(matcher.gsub("*", "%"))
|
|
24
|
+
Porpoise::KeyValueObject.not_expired.where(["`key` LIKE ?", matcher]).delete_all
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dump(key)
|
|
28
|
+
o = find_stored_object(key)
|
|
29
|
+
return nil if o.nil?
|
|
30
|
+
|
|
31
|
+
Marshal.dump(o.value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def exists(key, *other_keys)
|
|
35
|
+
all_keys = [key].concat(other_keys)
|
|
36
|
+
Porpoise::KeyValueObject.not_expired.
|
|
37
|
+
where(key: all_keys.map { |k| Porpoise::key_with_namespace(k) }).
|
|
38
|
+
count
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def expire(key, seconds)
|
|
42
|
+
o = find_stored_object(key)
|
|
43
|
+
return 0 if o.nil?
|
|
44
|
+
o.expiration_date = (Time.now + seconds)
|
|
45
|
+
|
|
46
|
+
return o.save ? 1 : 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def persist(key)
|
|
50
|
+
o = find_stored_object(key)
|
|
51
|
+
return 0 if o.nil? || o.expiration_date.blank?
|
|
52
|
+
|
|
53
|
+
o.expiration_date = nil
|
|
54
|
+
o.save ? 1 : 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def rename(key, newkey)
|
|
58
|
+
o = find_stored_object(key, true)
|
|
59
|
+
no = find_stored_object(newkey)
|
|
60
|
+
|
|
61
|
+
no.delete unless no.nil?
|
|
62
|
+
no = o.dup
|
|
63
|
+
no.key = newkey
|
|
64
|
+
o.delete
|
|
65
|
+
|
|
66
|
+
no.save
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def type(key)
|
|
70
|
+
o = find_stored_object(key)
|
|
71
|
+
return false if o.nil?
|
|
72
|
+
return o.data_type
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ttl(key)
|
|
76
|
+
o = find_stored_object(key)
|
|
77
|
+
return -2 if o.nil?
|
|
78
|
+
return -1 if o.expiration_date.nil?
|
|
79
|
+
return o.expiration_date - Time.now
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def keys(key_or_search_string)
|
|
83
|
+
if key_or_search_string.include?('*')
|
|
84
|
+
param = Porpoise::key_with_namespace(key_or_search_string.gsub('*', '%'))
|
|
85
|
+
ks = Porpoise::KeyValueObject.not_expired.
|
|
86
|
+
where(['`key` LIKE ?', param]).
|
|
87
|
+
pluck(:key)
|
|
88
|
+
|
|
89
|
+
return Porpoise::namespace? ? ks.map { |k| k.sub("#{Porpoise::namespace}:", '') } : ks
|
|
90
|
+
else
|
|
91
|
+
param = Porpoise::key_with_namespace(key_or_search_string)
|
|
92
|
+
ks = Porpoise::KeyValueObject.not_expired.where(key: param).pluck(:key)
|
|
93
|
+
return Porpoise::namespace? ? ks.map { |k| k.sub("#{Porpoise::namespace}:", '') } : ks
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def find_stored_object(key, raise_on_not_found = false)
|
|
100
|
+
key = Porpoise::key_with_namespace(key)
|
|
101
|
+
o = Porpoise::KeyValueObject.where(key: key).first
|
|
102
|
+
|
|
103
|
+
if raise_on_not_found && o.nil?
|
|
104
|
+
raise Porpoise::KeyNotFound.new("Key #{key} could not be found")
|
|
105
|
+
elsif !o.nil? && o.expired?
|
|
106
|
+
o.delete
|
|
107
|
+
o = nil
|
|
108
|
+
raise Porpoise::KeyNotFound.new("Key #{key} could not be found") if raise_on_not_found
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
return o
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
class Porpoise::KeyValueObject < ActiveRecord::Base
|
|
2
|
+
class Porpoise::TypeMismatch < StandardError
|
|
3
|
+
def initialize(msg)
|
|
4
|
+
super(msg)
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Porpoise::KeyNotFound < StandardError
|
|
9
|
+
def initialize(msg)
|
|
10
|
+
super(msg)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
self.primary_key = 'key'
|
|
15
|
+
|
|
16
|
+
# :nocov:
|
|
17
|
+
unless ENV['rack_env'] && ENV['rack_env'].eql?('test')
|
|
18
|
+
config = YAML.load(File.read('config/database.yml'))
|
|
19
|
+
establish_connection config["porpoise_#{Rails.env}"]
|
|
20
|
+
end
|
|
21
|
+
# :nocov:
|
|
22
|
+
|
|
23
|
+
serialize :value
|
|
24
|
+
|
|
25
|
+
attr_accessible :key, :value, :data_type, :expiration_date
|
|
26
|
+
|
|
27
|
+
after_initialize :check_data_type
|
|
28
|
+
before_validation :set_data_type
|
|
29
|
+
|
|
30
|
+
validates_inclusion_of :data_type, in: %w(String Hash Array)
|
|
31
|
+
|
|
32
|
+
scope :not_expired, conditions: ['(expiration_date IS NOT NULL AND expiration_date > ?) OR expiration_date IS NULL', Time.now]
|
|
33
|
+
|
|
34
|
+
def expired?
|
|
35
|
+
!self.expiration_date.nil? && self.expiration_date < Time.now
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def save
|
|
39
|
+
super
|
|
40
|
+
rescue ActiveRecord::RecordNotUnique
|
|
41
|
+
# catch race conditions
|
|
42
|
+
o = Porpoise::KeyValueObject.not_expired.where(key: self.key).first
|
|
43
|
+
o.value = self.value
|
|
44
|
+
o.expiration_date = self.expiration_date
|
|
45
|
+
o.save
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def check_data_type
|
|
51
|
+
if !self.data_type.nil? && self.value.class.name != self.data_type
|
|
52
|
+
raise Porpoise::TypeMismatch.new(
|
|
53
|
+
"#{self.value.class.name} is not of type #{self.data_type}"
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def set_data_type
|
|
59
|
+
self.data_type = self.value.class.name
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/porpoise/set.rb
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
module Porpoise
|
|
2
|
+
module Set
|
|
3
|
+
class << self
|
|
4
|
+
def sadd(key, *members)
|
|
5
|
+
o = find_stored_object(key)
|
|
6
|
+
previous_set = o.value.dup
|
|
7
|
+
o.value = o.value.concat(members).uniq
|
|
8
|
+
o.save
|
|
9
|
+
|
|
10
|
+
o.value.size - previous_set.size
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def scard(key)
|
|
14
|
+
o = find_stored_object(key)
|
|
15
|
+
o.value.size
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def sdiff(key, *other_keys)
|
|
19
|
+
o = find_stored_object(key)
|
|
20
|
+
current_set = o.value
|
|
21
|
+
other_keys = other_keys.map { |k| Porpoise::key_with_namespace(k) }
|
|
22
|
+
|
|
23
|
+
oo = Porpoise::KeyValueObject.not_expired.where(key: other_keys).all.index_by(&:key)
|
|
24
|
+
other_keys.each do |ok|
|
|
25
|
+
next unless oo.has_key?(ok)
|
|
26
|
+
current_set = current_set - oo[ok].value
|
|
27
|
+
end
|
|
28
|
+
current_set
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sdiffstore(destination, key, *other_keys)
|
|
32
|
+
o = find_stored_object(key)
|
|
33
|
+
current_set = o.value
|
|
34
|
+
other_keys = other_keys.map { |k| Porpoise::key_with_namespace(k) }
|
|
35
|
+
|
|
36
|
+
oo = Porpoise::KeyValueObject.not_expired.where(key: other_keys).all.index_by(&:key)
|
|
37
|
+
other_keys.each do |ok|
|
|
38
|
+
next unless oo.has_key?(ok)
|
|
39
|
+
current_set = current_set - oo[ok].value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
no = find_stored_object(destination)
|
|
43
|
+
no.value = current_set
|
|
44
|
+
no.save
|
|
45
|
+
no.value.size
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sinter(key, *other_keys)
|
|
49
|
+
o = find_stored_object(key)
|
|
50
|
+
current_set = o.value
|
|
51
|
+
other_keys = other_keys.map { |k| Porpoise::key_with_namespace(k) }
|
|
52
|
+
|
|
53
|
+
oo = Porpoise::KeyValueObject.not_expired.where(key: other_keys).all.index_by(&:key)
|
|
54
|
+
other_keys.each do |ok|
|
|
55
|
+
next unless oo.has_key?(ok)
|
|
56
|
+
current_set = current_set & oo[ok].value
|
|
57
|
+
end
|
|
58
|
+
return current_set
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def sinterstore(destination, key, *other_keys)
|
|
62
|
+
o = find_stored_object(key)
|
|
63
|
+
current_set = o.value
|
|
64
|
+
other_keys = other_keys.map { |k| Porpoise::key_with_namespace(k) }
|
|
65
|
+
|
|
66
|
+
oo = Porpoise::KeyValueObject.not_expired.where(key: other_keys).all.index_by(&:key)
|
|
67
|
+
other_keys.each do |ok|
|
|
68
|
+
next unless oo.has_key?(ok)
|
|
69
|
+
current_set = current_set & oo[ok].value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
no = find_stored_object(destination)
|
|
73
|
+
no.value = current_set
|
|
74
|
+
no.save
|
|
75
|
+
no.value.size
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def sismember(key, member)
|
|
79
|
+
o = find_stored_object(key)
|
|
80
|
+
return o.value.include?(member) ? 1 : 0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def smembers(key)
|
|
84
|
+
o = find_stored_object(key)
|
|
85
|
+
return o.value
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def smove(source, destination, member)
|
|
89
|
+
Porpoise::KeyValueObject.transaction do
|
|
90
|
+
src = find_stored_object(source, false, true)
|
|
91
|
+
dst = find_stored_object(destination, false, true)
|
|
92
|
+
|
|
93
|
+
ele = src.value.delete(member)
|
|
94
|
+
return 0 if ele.nil?
|
|
95
|
+
|
|
96
|
+
dst.value << ele unless dst.value.include?(ele)
|
|
97
|
+
res = dst.save && src.save
|
|
98
|
+
|
|
99
|
+
return (ele && res) ? 1 : 0
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def spop(key, count = 1)
|
|
104
|
+
o = find_stored_object(key)
|
|
105
|
+
return nil if o.new_record?
|
|
106
|
+
|
|
107
|
+
pd = []
|
|
108
|
+
count.times do
|
|
109
|
+
pd.push(o.value.shuffle!.pop) if o.value.size > 0
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
o.save
|
|
113
|
+
|
|
114
|
+
return pd
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def srandmember(key, count = 1)
|
|
118
|
+
o = find_stored_object(key)
|
|
119
|
+
return [] if o.new_record?
|
|
120
|
+
|
|
121
|
+
return o.value.sample(count)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def srem(key, member, *other_members)
|
|
125
|
+
o = find_stored_object(key)
|
|
126
|
+
return 0 if o.new_record?
|
|
127
|
+
|
|
128
|
+
previous_set = o.value.dup
|
|
129
|
+
all_members = [member].concat(other_members)
|
|
130
|
+
o.value = o.value.reject { |v| all_members.include?(v) }
|
|
131
|
+
o.save
|
|
132
|
+
|
|
133
|
+
previous_set - o.value
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def sunion(key, *other_keys)
|
|
137
|
+
o = find_stored_object(key)
|
|
138
|
+
current_set = o.value.dup
|
|
139
|
+
other_keys = other_keys.map { |k| Porpoise::key_with_namespace(k) }
|
|
140
|
+
|
|
141
|
+
oo = Porpoise::KeyValueObject.not_expired.where(key: other_keys).all.index_by(&:key)
|
|
142
|
+
other_keys.each do |ok|
|
|
143
|
+
next unless oo.has_key?(ok)
|
|
144
|
+
current_set.concat(oo[ok].value)
|
|
145
|
+
end
|
|
146
|
+
current_set.uniq
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def sunionstore(destination, key, *other_keys)
|
|
150
|
+
o = find_stored_object(key)
|
|
151
|
+
current_set = o.value.dup
|
|
152
|
+
other_keys = other_keys.map { |k| Porpoise::key_with_namespace(k) }
|
|
153
|
+
|
|
154
|
+
oo = Porpoise::KeyValueObject.not_expired.where(key: other_keys).all.index_by(&:key)
|
|
155
|
+
other_keys.each do |ok|
|
|
156
|
+
next unless oo.has_key?(ok)
|
|
157
|
+
current_set.concat(oo[ok].value)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
no = find_stored_object(destination)
|
|
161
|
+
no.value = current_set.uniq
|
|
162
|
+
no.save
|
|
163
|
+
no.value.size
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def find_stored_object(key,
|
|
169
|
+
raise_on_type_mismatch = true,
|
|
170
|
+
raise_on_not_found = false)
|
|
171
|
+
|
|
172
|
+
key = Porpoise::key_with_namespace(key)
|
|
173
|
+
o = Porpoise::KeyValueObject.not_expired.where(key: key).first
|
|
174
|
+
|
|
175
|
+
if raise_on_type_mismatch && !o.nil? && o.data_type != 'Array'
|
|
176
|
+
raise Porpoise::TypeMismatch.new(
|
|
177
|
+
"Key #{key} is not of type Set (is #{o.data_type})"
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
if raise_on_not_found && o.nil?
|
|
182
|
+
raise Porpoise::KeyNotFound.new("Key #{key} could not be found")
|
|
183
|
+
elsif o.nil?
|
|
184
|
+
o = Porpoise::KeyValueObject.new(key: key, value: ::Array.new)
|
|
185
|
+
elsif o.expired?
|
|
186
|
+
o.delete
|
|
187
|
+
o = Porpoise::KeyValueObject.new(key: key, value: ::Array.new)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
return o
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
module Porpoise
|
|
2
|
+
module String
|
|
3
|
+
class << self
|
|
4
|
+
def append(key, value)
|
|
5
|
+
o = find_stored_object(key)
|
|
6
|
+
o.value += value
|
|
7
|
+
o.save
|
|
8
|
+
|
|
9
|
+
o.value.size
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def decr(key)
|
|
13
|
+
o = find_stored_object(key)
|
|
14
|
+
o.value = (o.value.to_i - 1).to_s
|
|
15
|
+
o.save
|
|
16
|
+
|
|
17
|
+
o.value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def decrby(key, decrement)
|
|
21
|
+
o = find_stored_object(key)
|
|
22
|
+
o.value = (o.value.to_i - decrement.to_i).to_s
|
|
23
|
+
o.save
|
|
24
|
+
|
|
25
|
+
o.value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get(key)
|
|
29
|
+
o = find_stored_object(key)
|
|
30
|
+
return nil if o.new_record?
|
|
31
|
+
o.value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def getrange(key, first, last)
|
|
35
|
+
o = find_stored_object(key)
|
|
36
|
+
return nil if o.new_record?
|
|
37
|
+
o.value[first..last]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def getset(key, value)
|
|
41
|
+
o = find_stored_object(key)
|
|
42
|
+
return nil if o.new_record?
|
|
43
|
+
|
|
44
|
+
ov = o.value
|
|
45
|
+
o.value = value
|
|
46
|
+
o.save
|
|
47
|
+
|
|
48
|
+
return ov
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def incr(key)
|
|
52
|
+
o = find_stored_object(key)
|
|
53
|
+
o.value = (o.value.to_i + 1).to_s
|
|
54
|
+
o.save
|
|
55
|
+
|
|
56
|
+
o.value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def incrby(key, increment)
|
|
60
|
+
o = find_stored_object(key)
|
|
61
|
+
o.value = (o.value.to_i + increment.to_i).to_s
|
|
62
|
+
o.save
|
|
63
|
+
|
|
64
|
+
o.value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def mget(key, *other_keys)
|
|
68
|
+
o = find_stored_object(key)
|
|
69
|
+
values = o.new_record? ? [nil] : [o.value]
|
|
70
|
+
other_keys = other_keys.map { |k| Porpoise::key_with_namespace(k) }
|
|
71
|
+
|
|
72
|
+
oo = Porpoise::KeyValueObject.not_expired.where(key: other_keys).all.index_by(&:key)
|
|
73
|
+
other_keys.each do |ok|
|
|
74
|
+
values << (oo.has_key?(ok) ? oo[ok].value : nil)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return values
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mset(key, value, *other_keys_and_values)
|
|
81
|
+
Porpoise::KeyValueObject.transaction do
|
|
82
|
+
o = find_stored_object(key)
|
|
83
|
+
o.value = value
|
|
84
|
+
o.save
|
|
85
|
+
|
|
86
|
+
new_keys_and_values = ::Hash[*other_keys_and_values]
|
|
87
|
+
new_keys_and_values.each do |nk, nv|
|
|
88
|
+
oo = find_stored_object(nk)
|
|
89
|
+
oo.value = nv
|
|
90
|
+
oo.save
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def setex(key, seconds, value)
|
|
98
|
+
o = find_stored_object(key)
|
|
99
|
+
o.value = value
|
|
100
|
+
o.expiration_date = Time.now + seconds
|
|
101
|
+
return o.save
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def set(key, value, ex = nil, px = nil, nx_or_xx = nil)
|
|
105
|
+
o = find_stored_object(key, false)
|
|
106
|
+
o.value = value.to_s
|
|
107
|
+
|
|
108
|
+
if nx_or_xx
|
|
109
|
+
if nx_or_xx.downcase.eql?('nx')
|
|
110
|
+
return nil if !o.new_record?
|
|
111
|
+
elsif nx_or_xx.downcase.eql?('xx')
|
|
112
|
+
return nil if o.new_record?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
o.expiration_date = (Time.now + ex) unless ex.nil?
|
|
117
|
+
o.expiration_date = (Time.now + (px / 1000)) unless px.nil?
|
|
118
|
+
|
|
119
|
+
o.save
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def strlen(key)
|
|
123
|
+
o = find_stored_object(key)
|
|
124
|
+
o.value.size
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def find_stored_object(key,
|
|
130
|
+
raise_on_type_mismatch = true,
|
|
131
|
+
raise_on_not_found = false)
|
|
132
|
+
|
|
133
|
+
key = Porpoise::key_with_namespace(key)
|
|
134
|
+
o = Porpoise::KeyValueObject.where(key: key).first
|
|
135
|
+
|
|
136
|
+
if raise_on_type_mismatch && !o.nil? && o.data_type != 'String'
|
|
137
|
+
raise Porpoise::TypeMismatch.new(
|
|
138
|
+
"Key #{key} is not of type String (is #{o.data_type})"
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if raise_on_not_found && o.nil?
|
|
143
|
+
raise Porpoise::KeyNotFound.new("Key #{key} could not be found")
|
|
144
|
+
elsif o.nil?
|
|
145
|
+
o = Porpoise::KeyValueObject.new(key: key, value: ::String.new)
|
|
146
|
+
elsif o.expired?
|
|
147
|
+
o.delete
|
|
148
|
+
o = Porpoise::KeyValueObject.new(key: key, value: ::String.new)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
return o
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
data/lib/porpoise.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "porpoise/version"
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_support"
|
|
5
|
+
require "mysql2"
|
|
6
|
+
|
|
7
|
+
# Porpoise specific
|
|
8
|
+
require "porpoise/key_value_object"
|
|
9
|
+
require "porpoise/util"
|
|
10
|
+
require "porpoise/key"
|
|
11
|
+
require "porpoise/string"
|
|
12
|
+
require "porpoise/hash"
|
|
13
|
+
require "porpoise/set"
|
|
14
|
+
require "active_support/cache/porpoise_store"
|
|
15
|
+
|
|
16
|
+
module Porpoise
|
|
17
|
+
class << self
|
|
18
|
+
def with_namespace(namespace)
|
|
19
|
+
Thread.current[:namespace] = namespace.to_s
|
|
20
|
+
res = yield
|
|
21
|
+
Thread.current[:namespace] = nil
|
|
22
|
+
res
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def namespace
|
|
26
|
+
Thread.current[:namespace]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def namespace?
|
|
30
|
+
self.namespace && !self.namespace.blank?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def key_with_namespace(key)
|
|
34
|
+
self.namespace? ? "#{self.namespace}:#{key}" : key
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/porpoise.gemspec
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'porpoise/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "porpoise"
|
|
8
|
+
spec.version = Porpoise::VERSION
|
|
9
|
+
spec.authors = ["Wessel van Heerde"]
|
|
10
|
+
spec.email = ["wessel.van.heerde@sentia.com"]
|
|
11
|
+
spec.licenses = ['MIT']
|
|
12
|
+
spec.summary = "MySQL key/value store with a Redis compatible interface. Store and access objects in a Redis like way using MySQL storage. Don't use for high performance, use for easy multi master clustering."
|
|
13
|
+
spec.homepage = "https://dev-git.sentia.com/rubygems/porpoise"
|
|
14
|
+
|
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
|
17
|
+
end
|
|
18
|
+
spec.bindir = "exe"
|
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
20
|
+
spec.require_paths = ["lib"]
|
|
21
|
+
|
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
|
25
|
+
spec.add_development_dependency "sqlite3", "~> 1.3"
|
|
26
|
+
spec.add_development_dependency "simplecov", "~> 0"
|
|
27
|
+
spec.add_development_dependency "rspec-benchmark", "~> 0"
|
|
28
|
+
|
|
29
|
+
spec.add_dependency 'mysql2', '~> 0.3.20'
|
|
30
|
+
spec.add_dependency 'activerecord', "~> 3.2"
|
|
31
|
+
spec.add_dependency 'activesupport', "~> 3.2"
|
|
32
|
+
spec.add_dependency "rails", '~> 3.2'
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: porpoise
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.9.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Wessel van Heerde
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-10-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.13'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.13'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '10.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '10.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.2'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.2'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: sqlite3
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.3'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.3'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: simplecov
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rspec-benchmark
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: mysql2
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 0.3.20
|
|
104
|
+
type: :runtime
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 0.3.20
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: activerecord
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '3.2'
|
|
118
|
+
type: :runtime
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '3.2'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: activesupport
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '3.2'
|
|
132
|
+
type: :runtime
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '3.2'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: rails
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '3.2'
|
|
146
|
+
type: :runtime
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '3.2'
|
|
153
|
+
description:
|
|
154
|
+
email:
|
|
155
|
+
- wessel.van.heerde@sentia.com
|
|
156
|
+
executables: []
|
|
157
|
+
extensions: []
|
|
158
|
+
extra_rdoc_files: []
|
|
159
|
+
files:
|
|
160
|
+
- ".gitignore"
|
|
161
|
+
- Dockerfile
|
|
162
|
+
- Gemfile
|
|
163
|
+
- LICENSE
|
|
164
|
+
- README.md
|
|
165
|
+
- Rakefile
|
|
166
|
+
- bin/console
|
|
167
|
+
- bin/setup
|
|
168
|
+
- lib/active_support/cache/porpoise_store.rb
|
|
169
|
+
- lib/generators/porpoise/install_generator.rb
|
|
170
|
+
- lib/generators/porpoise/templates/migration.rb
|
|
171
|
+
- lib/porpoise.rb
|
|
172
|
+
- lib/porpoise/hash.rb
|
|
173
|
+
- lib/porpoise/key.rb
|
|
174
|
+
- lib/porpoise/key_value_object.rb
|
|
175
|
+
- lib/porpoise/set.rb
|
|
176
|
+
- lib/porpoise/string.rb
|
|
177
|
+
- lib/porpoise/util.rb
|
|
178
|
+
- lib/porpoise/version.rb
|
|
179
|
+
- porpoise.gemspec
|
|
180
|
+
homepage: https://dev-git.sentia.com/rubygems/porpoise
|
|
181
|
+
licenses:
|
|
182
|
+
- MIT
|
|
183
|
+
metadata: {}
|
|
184
|
+
post_install_message:
|
|
185
|
+
rdoc_options: []
|
|
186
|
+
require_paths:
|
|
187
|
+
- lib
|
|
188
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
189
|
+
requirements:
|
|
190
|
+
- - ">="
|
|
191
|
+
- !ruby/object:Gem::Version
|
|
192
|
+
version: '0'
|
|
193
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
194
|
+
requirements:
|
|
195
|
+
- - ">="
|
|
196
|
+
- !ruby/object:Gem::Version
|
|
197
|
+
version: '0'
|
|
198
|
+
requirements: []
|
|
199
|
+
rubyforge_project:
|
|
200
|
+
rubygems_version: 2.5.2
|
|
201
|
+
signing_key:
|
|
202
|
+
specification_version: 4
|
|
203
|
+
summary: MySQL key/value store with a Redis compatible interface. Store and access
|
|
204
|
+
objects in a Redis like way using MySQL storage. Don't use for high performance,
|
|
205
|
+
use for easy multi master clustering.
|
|
206
|
+
test_files: []
|