delta_cache 1.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.
- data/README.md +39 -0
- data/delta_cache.gemspec +39 -0
- data/examples/callback.rb +76 -0
- data/examples/facebook/callback.rb +76 -0
- data/examples/facebook/config.ru +6 -0
- data/examples/facebook/facebook.rb +96 -0
- data/examples/notifications/notifications.rb +68 -0
- data/lib/delta_cache/db/cassandra.rb +139 -0
- data/lib/delta_cache/db/redis.rb +79 -0
- data/lib/delta_cache/version.rb +3 -0
- data/lib/delta_cache.rb +136 -0
- metadata +60 -0
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Keep track of deltas and tombstones for an array of data
|
2
|
+
|
3
|
+
A cache that keeps track of deltas and tombstones for an array of data. Deltas and tombstones can be retrieved from the cache using a last-modified timestamp.
|
4
|
+
|
5
|
+
## Configuration
|
6
|
+
|
7
|
+
### Redis
|
8
|
+
|
9
|
+
Connection
|
10
|
+
|
11
|
+
DeltaCache.connection = Redis.new(:host => "your-host-ip")
|
12
|
+
|
13
|
+
### Cassandra
|
14
|
+
|
15
|
+
Setup Cassandra Keyspaces and Column Families
|
16
|
+
|
17
|
+
create keyspace DeltaCache;
|
18
|
+
use DeltaCache;
|
19
|
+
create column family Cache;
|
20
|
+
create column family Deltas;
|
21
|
+
|
22
|
+
create keyspace DeltaCacheTest;
|
23
|
+
use DeltaCacheTest;
|
24
|
+
create column family Cache;
|
25
|
+
create column family Deltas;
|
26
|
+
|
27
|
+
Connection
|
28
|
+
|
29
|
+
DeltaCache.connection = Cassandra.new('DeltaCache')
|
30
|
+
|
31
|
+
### Namespace
|
32
|
+
|
33
|
+
DeltaCache.cache_name = "your-cache-namespace"
|
34
|
+
|
35
|
+
### Logger
|
36
|
+
|
37
|
+
DeltaCache.logger = Logger.new(STDOUT, Logger::DEBUG)
|
38
|
+
|
39
|
+
See emamples directory for more details.
|
data/delta_cache.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
# -*- encoding: utf-8 -*-
|
3
|
+
$:.push('lib')
|
4
|
+
require "delta_cache/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "delta_cache"
|
8
|
+
s.version = DeltaCache::VERSION.dup
|
9
|
+
s.date = "2012-02-01"
|
10
|
+
s.summary = "A cache that keeps track of deltas and tombstones for an array of data."
|
11
|
+
s.email = "john.critz@gmail.com"
|
12
|
+
s.homepage = ""
|
13
|
+
s.authors = ['John Critz']
|
14
|
+
|
15
|
+
s.description = <<-EOF
|
16
|
+
A cache that keeps track of deltas and tombstones for an array of data. Deltas and tombstones can be retrieved from the cache using a last-modified timestamp.
|
17
|
+
EOF
|
18
|
+
|
19
|
+
dependencies = []
|
20
|
+
|
21
|
+
s.files = Dir['**/*']
|
22
|
+
s.test_files = Dir['test/**/*'] + Dir['spec/**/*']
|
23
|
+
s.executables = Dir['bin/*'].map { |f| File.basename(f) }
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
|
26
|
+
|
27
|
+
## Make sure you can build the gem on older versions of RubyGems too:
|
28
|
+
s.rubygems_version = "1.8.10"
|
29
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
30
|
+
s.specification_version = 3 if s.respond_to? :specification_version
|
31
|
+
|
32
|
+
dependencies.each do |type, name, version|
|
33
|
+
if s.respond_to?("add_#{type}_dependency")
|
34
|
+
s.send("add_#{type}_dependency", name, version)
|
35
|
+
else
|
36
|
+
s.add_dependency(name, version)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require './facebook'
|
3
|
+
|
4
|
+
class Callback
|
5
|
+
|
6
|
+
attr_accessor :last_modified
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
init(env)
|
10
|
+
puts env.inspect
|
11
|
+
puts env["rack.input"].input.inspect
|
12
|
+
puts @req.inspect
|
13
|
+
puts @req.form_data?
|
14
|
+
puts @req.params.inspect
|
15
|
+
|
16
|
+
if is_valid_request?
|
17
|
+
|
18
|
+
if is_challenge_request?
|
19
|
+
[200, {'Content-Type' => 'text/plain'}, [@params["hub.challenge"]]]
|
20
|
+
|
21
|
+
else
|
22
|
+
changes = @params["entry"]
|
23
|
+
changes.each do |entry|
|
24
|
+
|
25
|
+
if is_valid_change?(entry)
|
26
|
+
fb_id = Integer(entry["uid"])
|
27
|
+
|
28
|
+
# update cache
|
29
|
+
friends_info = Facebook.get_friends(fb_id)
|
30
|
+
DeltaCache.new(:cache_prefix => "facebook", :key_id => fb_id).update(friends_info, fb_id)
|
31
|
+
|
32
|
+
# show changes
|
33
|
+
puts DeltaCache.new(
|
34
|
+
fb_id, :last_modified => self.last_modified
|
35
|
+
).get_info(self.last_modified)
|
36
|
+
|
37
|
+
# store last modified timestamp
|
38
|
+
self.last_modified = DeltaCache.new(fb_id).get_last_modified
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
[200, {'Content-Type' => 'text/plain'}, []]
|
43
|
+
end
|
44
|
+
|
45
|
+
else
|
46
|
+
[404, {'Content-Type' => 'text/plain'}, []]
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def init(env)
|
52
|
+
@req = Rack::Request.new(env)
|
53
|
+
@params = @req.params if @req
|
54
|
+
end
|
55
|
+
|
56
|
+
def is_valid_request?
|
57
|
+
@req && @params
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_challenge_request?
|
61
|
+
return false if @params["hub.mode"].nil? || @params["hub.mode"].empty?
|
62
|
+
return false if @params["hub.challenge"].nil? || @params["hub.challenge"].empty?
|
63
|
+
return false if @params["hub.verify_token"].nil? || (token = @params["hub.verify_token"]).empty?
|
64
|
+
return FACEBOOK_CONFIG[:subscription_token] == token
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_valid_change?(entry)
|
68
|
+
fb_id = Integer(entry["uid"])
|
69
|
+
fields = Array(entry["changed_fields"])
|
70
|
+
|
71
|
+
return false if fb_id.empty?
|
72
|
+
return false if fields.empty?
|
73
|
+
return fields.include?("friends")
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require './facebook'
|
3
|
+
|
4
|
+
class Callback
|
5
|
+
|
6
|
+
attr_accessor :last_modified
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
init(env)
|
10
|
+
# puts env.inspect
|
11
|
+
# puts env["rack.input"].input.inspect
|
12
|
+
# puts @req.inspect
|
13
|
+
# puts @req.form_data?
|
14
|
+
# puts @req.params.inspect
|
15
|
+
|
16
|
+
if is_valid_request?
|
17
|
+
|
18
|
+
if is_challenge_request?
|
19
|
+
[200, {'Content-Type' => 'text/plain'}, [@params["hub.challenge"]]]
|
20
|
+
|
21
|
+
else
|
22
|
+
changes = @params["entry"]
|
23
|
+
changes.each do |entry|
|
24
|
+
|
25
|
+
if is_valid_change?(entry)
|
26
|
+
fb_id = Integer(entry["uid"])
|
27
|
+
|
28
|
+
# update cache
|
29
|
+
friends_info = Facebook.get_friends(fb_id)
|
30
|
+
DeltaCache::Cache.new(fb_id).update(friends_info)
|
31
|
+
|
32
|
+
# show changes
|
33
|
+
puts DeltaCache::Cache.new(fb_id).get_info(self.last_modified)
|
34
|
+
|
35
|
+
# store last modified timestamp
|
36
|
+
self.last_modified = DeltaCache::Cache.new(fb_id).get_last_modified
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
[200, {'Content-Type' => 'text/plain'}, []]
|
41
|
+
end
|
42
|
+
|
43
|
+
else
|
44
|
+
[404, {'Content-Type' => 'text/plain'}, []]
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def init(env)
|
50
|
+
@req = Rack::Request.new(env)
|
51
|
+
@params = @req.params if @req
|
52
|
+
DeltaCache.connection = Redis.new(:host => "127.0.0.1")
|
53
|
+
DeltaCache.cache_name = "facebook"
|
54
|
+
end
|
55
|
+
|
56
|
+
def is_valid_request?
|
57
|
+
@req && @params
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_challenge_request?
|
61
|
+
return false if @params["hub.mode"].nil? || @params["hub.mode"].empty?
|
62
|
+
return false if @params["hub.challenge"].nil? || @params["hub.challenge"].empty?
|
63
|
+
return false if @params["hub.verify_token"].nil? || (token = @params["hub.verify_token"]).empty?
|
64
|
+
return FACEBOOK_CONFIG[:subscription_token] == token
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_valid_change?(entry)
|
68
|
+
fb_id = Integer(entry["uid"])
|
69
|
+
fields = Array(entry["changed_fields"])
|
70
|
+
|
71
|
+
return false if fb_id.empty?
|
72
|
+
return false if fields.empty?
|
73
|
+
return fields.include?("friends")
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
####
|
2
|
+
# Enable/disable Facebook subscriptions for a Facebook application
|
3
|
+
# so you will be notified of any changes to the users who have
|
4
|
+
# authorized the given Facebook app.
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
# Configure the FACEBOOK_CONFIG hash with the app information
|
8
|
+
# and then execute the ruby script.
|
9
|
+
#
|
10
|
+
####
|
11
|
+
|
12
|
+
require 'net/http'
|
13
|
+
require 'uri'
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'fb_graph'
|
17
|
+
require 'redis'
|
18
|
+
|
19
|
+
require '../../lib/delta_cache'
|
20
|
+
|
21
|
+
FACEBOOK_CONFIG = {
|
22
|
+
:app_id => "your-app-id",
|
23
|
+
:access_token => "your-fb-access-token",
|
24
|
+
:subscription_callback_url => "http://your-public-ip/",
|
25
|
+
:subscription_token => "any-token-to-validate-the-fb-request"
|
26
|
+
}
|
27
|
+
|
28
|
+
class Facebook
|
29
|
+
|
30
|
+
def self.subscribe!
|
31
|
+
uri = URI.parse("https://graph.facebook.com/#{FACEBOOK_CONFIG[:app_id]}/subscriptions")
|
32
|
+
form_data = {
|
33
|
+
:object => "user",
|
34
|
+
:fields => "name,friends",
|
35
|
+
:access_token => FACEBOOK_CONFIG[:access_token],
|
36
|
+
:callback_url => FACEBOOK_CONFIG[:subscription_callback_url],
|
37
|
+
:verify_token => FACEBOOK_CONFIG[:subscription_token]
|
38
|
+
}
|
39
|
+
|
40
|
+
HTTP.new(uri).post(form_data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.unsubscribe!
|
44
|
+
uri = URI.parse("https://graph.facebook.com/#{FACEBOOK_CONFIG[:app_id]}/subscriptions")
|
45
|
+
form_data = {
|
46
|
+
:object => "user",
|
47
|
+
:access_token => FACEBOOK_CONFIG[:access_token]
|
48
|
+
}
|
49
|
+
|
50
|
+
HTTP.new(uri).delete(form_data)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.get_friends(fb_id)
|
54
|
+
FbGraph::User.fetch(fb_id, :access_token => FACEBOOK_CONFIG[:access_token])
|
55
|
+
end
|
56
|
+
|
57
|
+
class HTTP
|
58
|
+
|
59
|
+
attr_accessor :uri, :http
|
60
|
+
|
61
|
+
def initialize(uri)
|
62
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
63
|
+
http.use_ssl = true
|
64
|
+
http.read_timeout = 120
|
65
|
+
|
66
|
+
self.uri = uri
|
67
|
+
self.http = http
|
68
|
+
end
|
69
|
+
|
70
|
+
def post(form_data)
|
71
|
+
request = Net::HTTP::Post.new(self.uri.request_uri)
|
72
|
+
request.set_form_data(form_data)
|
73
|
+
response = self.http.request(request)
|
74
|
+
|
75
|
+
puts "\nForm Data"
|
76
|
+
puts form_data.inspect
|
77
|
+
puts "--"
|
78
|
+
|
79
|
+
puts "\nResponse"
|
80
|
+
puts response
|
81
|
+
puts "--"
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete(form_data)
|
85
|
+
request = Net::HTTP::Delete.new(self.uri.request_uri)
|
86
|
+
request.set_form_data(form_data)
|
87
|
+
response = self.http.request(request)
|
88
|
+
|
89
|
+
puts "\nResponse"
|
90
|
+
puts response
|
91
|
+
puts "--"
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'redis'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require File.expand_path('../../lib/delta_cache.rb')
|
6
|
+
|
7
|
+
DeltaCache.connection = Redis.new(:host => "127.0.0.1")
|
8
|
+
DeltaCache.cache_name = "notifications"
|
9
|
+
|
10
|
+
class Notifications
|
11
|
+
|
12
|
+
attr_accessor :cache
|
13
|
+
|
14
|
+
def initialize(cache_id)
|
15
|
+
self.cache = DeltaCache::Cache.new(cache_id)
|
16
|
+
|
17
|
+
## flush and initialize cache
|
18
|
+
cache.flush!
|
19
|
+
|
20
|
+
puts "\nInitializing cache..."
|
21
|
+
notifications = [
|
22
|
+
{:name => "John", :message => "Be my friend."},
|
23
|
+
]
|
24
|
+
update_and_print(notifications)
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_to_cache
|
28
|
+
puts "\nAdding Steve and Bill to cache..."
|
29
|
+
|
30
|
+
lm = cache.get_last_modified
|
31
|
+
puts "\n Deltas since: #{lm}"
|
32
|
+
notifications = [
|
33
|
+
{:name => "John", :message => "Be my friend."},
|
34
|
+
{:name => "Steve", :message => "Be my friend."},
|
35
|
+
{:name => "Bill", :message => "Be my friend."}
|
36
|
+
]
|
37
|
+
|
38
|
+
update_and_print(notifications)
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete_from_cache
|
42
|
+
puts "\nDeleting John from cache..."
|
43
|
+
|
44
|
+
lm = cache.get_last_modified
|
45
|
+
puts "\n Tombstones since: #{lm}"
|
46
|
+
notifications = [
|
47
|
+
{:name => "Steve", :message => "Be my friend."},
|
48
|
+
{:name => "Bill", :message => "Be my friend."}
|
49
|
+
]
|
50
|
+
|
51
|
+
update_and_print(notifications)
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_and_print(data)
|
55
|
+
lm = cache.get_last_modified
|
56
|
+
cache.update(data)
|
57
|
+
puts " " + cache.get_info(lm).inspect
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
n = Notifications.new(1)
|
63
|
+
sleep 2
|
64
|
+
|
65
|
+
n.add_to_cache
|
66
|
+
sleep 2
|
67
|
+
|
68
|
+
n.delete_from_cache
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# Stores friend cache data in a Cassandra keyspace. The advantage here is
|
2
|
+
# the cache can grow larger than the memory on one machine. The disadvantage
|
3
|
+
# is that you have to set up multiple machines and make some decisions about
|
4
|
+
# acceptable thresholds around consistency and atomicity.
|
5
|
+
#
|
6
|
+
# This module requires initialization, like so:
|
7
|
+
#
|
8
|
+
# DeltaCache.connection = Cassandra.new('DeltaCache')
|
9
|
+
# DeltaCache.logger = Logger.new(STDOUT, Logger::DEBUG)
|
10
|
+
#
|
11
|
+
# You only need to set the logger if you need to debug.
|
12
|
+
#
|
13
|
+
# ## Storage
|
14
|
+
#
|
15
|
+
# This adapter uses two column families: `:Cache` and `:Deltas`.
|
16
|
+
#
|
17
|
+
# The `:Cache` CF stores each cached object as a JSON blob. Each row is keyed
|
18
|
+
# by the SHA1 hash of the blob. The blob itself is stored in a column named
|
19
|
+
# `blob`. These rows won't ever get too wide, so this CF is a good candidate
|
20
|
+
# for row caching by Cassandra.
|
21
|
+
#
|
22
|
+
# The `:Deltas` CF stores changes to objects cached for each user. There are
|
23
|
+
# two rows per user; one tracks newly cached objects and the other tracks
|
24
|
+
# cached objects that have been removed. Each row contains a pair of columns
|
25
|
+
# per cached object. One maps a timestamp to a cached object and is used for
|
26
|
+
# caching the most recent changes to a user's cached objects. The other maps
|
27
|
+
# cached objects back to a timestamp and is used to find delta entries to
|
28
|
+
# remove if a cached object is marked as removed.
|
29
|
+
|
30
|
+
class DeltaCache::CassandraDB
|
31
|
+
|
32
|
+
MAX_DELTAS = 10_000
|
33
|
+
|
34
|
+
attr_accessor :cache_id
|
35
|
+
|
36
|
+
def initialize(cache_id)
|
37
|
+
self.cache_id = cache_id
|
38
|
+
end
|
39
|
+
|
40
|
+
def connection
|
41
|
+
DeltaCache.connection
|
42
|
+
end
|
43
|
+
|
44
|
+
def logger
|
45
|
+
DeltaCache.logger
|
46
|
+
end
|
47
|
+
|
48
|
+
def cache_name
|
49
|
+
DeltaCache.cache_name
|
50
|
+
end
|
51
|
+
|
52
|
+
# Given a key ID, generate a key into `:Deltas` for cached objects.
|
53
|
+
def info_key
|
54
|
+
["cache", cache_name, cache_id].join(":")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Given a key ID, generate a key into `:Deltas` for deleted cache objects.
|
58
|
+
def deleted_info_key
|
59
|
+
["deleted", cache_name, cache_id].join(":")
|
60
|
+
end
|
61
|
+
|
62
|
+
# Store a cached object.
|
63
|
+
def set(data)
|
64
|
+
data = data.to_json
|
65
|
+
cache_key = Digest::SHA1.hexdigest(data)
|
66
|
+
|
67
|
+
log("insert :Cache, #{cache_key} -> {'blob' => #{data.inspect}}")
|
68
|
+
connection.insert(:Cache, cache_key, { "blob" => data })
|
69
|
+
cache_key
|
70
|
+
end
|
71
|
+
|
72
|
+
# Add a cached object to a delta timeline.
|
73
|
+
def add(key, timestamp, value)
|
74
|
+
columns = { timestamp_name(timestamp) => value, cache_name(value) => timestamp.to_f.to_s }
|
75
|
+
|
76
|
+
log("insert :Deltas, #{key} -> #{columns.inspect}")
|
77
|
+
connection.insert(:Deltas, key, columns)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Remove a cached object from a delta timeline.
|
81
|
+
def rem(key, value)
|
82
|
+
log("get :Deltas, #{key}, #{cache_name(value)}")
|
83
|
+
timestamp = connection.get(:Deltas, key, cache_name(value))
|
84
|
+
|
85
|
+
log("remove :Deltas, #{key} -> #{timestamp_name(timestamp)}")
|
86
|
+
connection.remove(:Deltas, key, timestamp_name(timestamp))
|
87
|
+
|
88
|
+
log("remove :Deltas, #{key} -> #{cache_name(value)}")
|
89
|
+
connection.remove(:Deltas, key, cache_name(value))
|
90
|
+
end
|
91
|
+
|
92
|
+
# Fetch multiple cached objects.
|
93
|
+
def get(keys)
|
94
|
+
log("get :Cache, #{keys.inspect}")
|
95
|
+
connection.multi_get(:Cache, Array(keys)).values.map { |v| v['blob'] }
|
96
|
+
end
|
97
|
+
|
98
|
+
# Find all the cached objects referenced by a delta timeline.
|
99
|
+
def get_rev_range(key, most_recent, last_modified)
|
100
|
+
log("get :Deltas, #{key}, :reversed => true, :start => #{timestamp_name(most_recent)}, :finish => #{timestamp_name(last_modified)}")
|
101
|
+
connection.get(:Deltas, key, :reversed => true, :start => timestamp_name(most_recent), :finish => timestamp_name(last_modified), :count => MAX_DELTAS).values
|
102
|
+
end
|
103
|
+
|
104
|
+
# Get a mapping of cached objects to delta timestamps for a given user.
|
105
|
+
def get_timestamp(key, value)
|
106
|
+
log("get :Deltas, #{key}, #{cache_name(value)}")
|
107
|
+
score = connection.get(:Deltas, key, cache_name(value))
|
108
|
+
Time.at(score.to_f).utc
|
109
|
+
end
|
110
|
+
|
111
|
+
# Remove an object from the cache.
|
112
|
+
def del(key)
|
113
|
+
log("remove :Cache, #{key}")
|
114
|
+
connection.remove(:Cache, key)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Check if a delta cache exists for a user.
|
118
|
+
def exists(key)
|
119
|
+
log("exists? :Deltas, #{key}")
|
120
|
+
connection.exists?(:Deltas, key)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Given a timestamp, generate a column name for use in range queries on `:Deltas`.
|
124
|
+
def timestamp_name(timestamp)
|
125
|
+
["ts", timestamp.to_f].join(":")
|
126
|
+
end
|
127
|
+
|
128
|
+
# Given the SHA1 for a cached object, generate a key into `:Cache`.
|
129
|
+
def cache_name(value)
|
130
|
+
["cache", value].join(":")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Private: Log a message, if a logger is set.
|
134
|
+
def log(msg)
|
135
|
+
return if logger.nil?
|
136
|
+
logger.debug(msg)
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class DeltaCache::RedisDB
|
2
|
+
|
3
|
+
attr_accessor :cache_id
|
4
|
+
|
5
|
+
def initialize(cache_id)
|
6
|
+
self.cache_id = cache_id
|
7
|
+
end
|
8
|
+
|
9
|
+
def connection
|
10
|
+
DeltaCache.connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def cache_name
|
14
|
+
DeltaCache.cache_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def info_key
|
18
|
+
[
|
19
|
+
"delta_cache",
|
20
|
+
"info",
|
21
|
+
cache_name,
|
22
|
+
cache_id
|
23
|
+
].join(":")
|
24
|
+
end
|
25
|
+
|
26
|
+
def deleted_info_key
|
27
|
+
[
|
28
|
+
"delta_cache",
|
29
|
+
"deleted_info",
|
30
|
+
cache_name,
|
31
|
+
cache_id
|
32
|
+
].join(":")
|
33
|
+
end
|
34
|
+
|
35
|
+
# set parent key that holds the data
|
36
|
+
def set(data)
|
37
|
+
data = data.to_json
|
38
|
+
cache_key = Digest::SHA1.hexdigest(data)
|
39
|
+
connection.set(cache_key, data)
|
40
|
+
cache_key
|
41
|
+
end
|
42
|
+
|
43
|
+
# add key to a set
|
44
|
+
def add(key, timestamp, value)
|
45
|
+
connection.zadd(key, timestamp.to_i, value)
|
46
|
+
end
|
47
|
+
|
48
|
+
# remove key from a set
|
49
|
+
def rem(key, value)
|
50
|
+
connection.zrem(key, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
# get a list of keys
|
54
|
+
def get(keys)
|
55
|
+
connection.mget(*keys)
|
56
|
+
end
|
57
|
+
|
58
|
+
# get members from a set in reverse order
|
59
|
+
def get_rev_range(key, end_pos, start_pos)
|
60
|
+
connection.zrevrangebyscore(key, end_pos.to_i, start_pos.to_i)
|
61
|
+
end
|
62
|
+
|
63
|
+
# get the 'score' of the member in a set
|
64
|
+
def get_timestamp(key, value)
|
65
|
+
score = connection.zscore(key, value)
|
66
|
+
Time.at(score.to_i).utc
|
67
|
+
end
|
68
|
+
|
69
|
+
# delete a key
|
70
|
+
def del(key)
|
71
|
+
connection.del(key)
|
72
|
+
end
|
73
|
+
|
74
|
+
# does a key exist
|
75
|
+
def exists(key)
|
76
|
+
connection.exists(key)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/lib/delta_cache.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module DeltaCache
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'digest'
|
5
|
+
|
6
|
+
require 'delta_cache/db/redis'
|
7
|
+
require 'delta_cache/db/cassandra'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :connection, :logger, :cache_name
|
11
|
+
end
|
12
|
+
|
13
|
+
class Cache
|
14
|
+
|
15
|
+
attr_accessor :db, :cache_id
|
16
|
+
|
17
|
+
def initialize(cache_id)
|
18
|
+
self.cache_id = cache_id
|
19
|
+
|
20
|
+
raise "DeltaCache.connection is not defined." unless DeltaCache.connection
|
21
|
+
raise "DeltaCache.cache_name is not defined." unless DeltaCache.cache_name
|
22
|
+
|
23
|
+
class_name = DeltaCache.connection.class.name
|
24
|
+
if class_name =~ /redis/i
|
25
|
+
self.db = DeltaCache::RedisDB.new(cache_id)
|
26
|
+
elsif class_name =~ /cassandra/i
|
27
|
+
self.db = DeltaCache::CassandraDB.new(cache_id)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def info_key
|
32
|
+
db.info_key
|
33
|
+
end
|
34
|
+
|
35
|
+
def deleted_info_key
|
36
|
+
db.deleted_info_key
|
37
|
+
end
|
38
|
+
|
39
|
+
def exists?
|
40
|
+
db.exists(info_key)
|
41
|
+
end
|
42
|
+
|
43
|
+
# this will remove all cache for the given id
|
44
|
+
def flush!
|
45
|
+
db.del(info_key)
|
46
|
+
db.del(deleted_info_key)
|
47
|
+
end
|
48
|
+
|
49
|
+
def update(info)
|
50
|
+
cache_new(info)
|
51
|
+
# stores tombstones for deleted records
|
52
|
+
cache_deleted(info)
|
53
|
+
|
54
|
+
return true
|
55
|
+
end
|
56
|
+
|
57
|
+
def cache_new(info)
|
58
|
+
new_info = (info - get_info(nil, false))
|
59
|
+
new_info.each do |info|
|
60
|
+
info_id = set_info(info)
|
61
|
+
db.rem(deleted_info_key, info_id)
|
62
|
+
db.add(info_key, timestamp, info_id)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def cache_deleted(info)
|
67
|
+
deleted_info = (get_info(nil, false) - info)
|
68
|
+
deleted_info.each do |info|
|
69
|
+
info_id = set_info(info)
|
70
|
+
db.rem(info_key, info_id)
|
71
|
+
db.add(deleted_info_key, timestamp, info_id)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def timestamp(time=nil)
|
76
|
+
Time.parse((time || Time.now).to_s).utc
|
77
|
+
end
|
78
|
+
|
79
|
+
def set_info(info)
|
80
|
+
db.set(info)
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_info(last_modified=nil, show_deleted_flag=true)
|
84
|
+
return [] unless exists?
|
85
|
+
|
86
|
+
last_modified_time = if last_modified.nil?
|
87
|
+
Time.at(0)
|
88
|
+
else
|
89
|
+
timestamp(last_modified)
|
90
|
+
end
|
91
|
+
|
92
|
+
info = []
|
93
|
+
|
94
|
+
info_ids = db.get_rev_range(info_key, timestamp, last_modified_time + 1)
|
95
|
+
if info_ids.any?
|
96
|
+
info += db.get(info_ids).compact.map do |f|
|
97
|
+
hash = JSON.parse(f)
|
98
|
+
hash.merge!(:deleted => false) if show_deleted_flag
|
99
|
+
hash
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
if last_modified_time.to_i > 0
|
104
|
+
info_ids = db.get_rev_range(deleted_info_key, timestamp, last_modified_time + 1)
|
105
|
+
if info_ids.any?
|
106
|
+
info += db.get(info_ids).compact.map do |f|
|
107
|
+
hash = JSON.parse(f)
|
108
|
+
hash.merge!(:deleted => true) if show_deleted_flag
|
109
|
+
hash
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
info.map do |hash|
|
115
|
+
new_hash = Hash.new
|
116
|
+
hash.each do |k, v|
|
117
|
+
new_hash[k.to_sym] = v
|
118
|
+
end
|
119
|
+
new_hash
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_last_modified
|
124
|
+
info_ids = db.get_rev_range(info_key, timestamp, Time.at(0))
|
125
|
+
deleted_info_ids = db.get_rev_range(deleted_info_key, timestamp, Time.at(0))
|
126
|
+
|
127
|
+
last_modified = [ db.get_timestamp(info_key, info_ids.first),
|
128
|
+
db.get_timestamp(deleted_info_key, deleted_info_ids.first) ].compact
|
129
|
+
return Time.now.utc.httpdate unless last_modified.any?
|
130
|
+
|
131
|
+
Time.at(last_modified.max.to_i).utc.httpdate
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: delta_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Critz
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-01 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! 'A cache that keeps track of deltas and tombstones for an array of
|
15
|
+
data. Deltas and tombstones can be retrieved from the cache using a last-modified
|
16
|
+
timestamp.
|
17
|
+
|
18
|
+
'
|
19
|
+
email: john.critz@gmail.com
|
20
|
+
executables: []
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- delta_cache-1.0.gem
|
25
|
+
- delta_cache.gemspec
|
26
|
+
- examples/callback.rb
|
27
|
+
- examples/facebook/callback.rb
|
28
|
+
- examples/facebook/config.ru
|
29
|
+
- examples/facebook/facebook.rb
|
30
|
+
- examples/notifications/notifications.rb
|
31
|
+
- lib/delta_cache/db/cassandra.rb
|
32
|
+
- lib/delta_cache/db/redis.rb
|
33
|
+
- lib/delta_cache/version.rb
|
34
|
+
- lib/delta_cache.rb
|
35
|
+
- README.md
|
36
|
+
homepage: ''
|
37
|
+
licenses: []
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.8.10
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: A cache that keeps track of deltas and tombstones for an array of data.
|
60
|
+
test_files: []
|