stacks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MzVhYWZlNDQ4ZGRlOGVlNDM3ZmU1NWU0ZmMzZTMyMDVjOTk4ZTNkYg==
5
+ data.tar.gz: !binary |-
6
+ ZmFmNmYyZDRmMTM5YjU1NDAxZWFiNzY1MzdlNzlkMzU3Yjc2YjYyMA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ Yzg2ZmI5MDVkYTIxYmIwZTRkYzk3MGU1OTFjODY4NmExMTI5ZWNhOTU0MmVh
10
+ MDAwYjgzNzM5YjZkZWZhODAzMDA3YTk5N2Q0OGNiMmU1MThiNzQwYjhjNjYx
11
+ Y2RhYzczMzA2ZjUyNGU0YTViNGUzNDA2ZmVjN2NjZjYzMzczYjA=
12
+ data.tar.gz: !binary |-
13
+ YWY0NTJiN2ZkNTJhMDhkNzc2YTZmZWQyMzEyZDRmZTNjNGI3ZjNkMjlmYWEx
14
+ YzFiMjk0M2IwMWM3NmMxNGZlOTc3NWY3NTE4ZWQ0YjgyODUwMjk5MWZlMmUw
15
+ ZTc5Mzk1NzQ5ZTY2YjE3MWU1MmZiNjdjNTNkZDdmMDA4MDZhM2U=
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1 @@
1
+ stacks
@@ -0,0 +1 @@
1
+ ruby-1.9.3
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'activerecord'
4
+
5
+ group :development do
6
+ gem 'mock_redis', '~> 0.10.0'
7
+ gem 'simplecov', '~> 0.8.2'
8
+ gem 'sqlite3', '~> 1.3.8'
9
+ gem 'jeweler', '~> 1.8.4'
10
+ gem 'rdoc', '~> 3.12'
11
+ gem 'rspec', '~> 2.8.0'
12
+ gem 'pry', '~> 0.9.12.4'
13
+ end
@@ -0,0 +1,85 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activerecord (2.3.18)
5
+ activesupport (= 2.3.18)
6
+ activesupport (2.3.18)
7
+ addressable (2.3.5)
8
+ builder (3.2.2)
9
+ coderay (1.1.0)
10
+ diff-lcs (1.1.3)
11
+ docile (1.1.1)
12
+ faraday (0.8.8)
13
+ multipart-post (~> 1.2.0)
14
+ git (1.2.6)
15
+ github_api (0.10.1)
16
+ addressable
17
+ faraday (~> 0.8.1)
18
+ hashie (>= 1.2)
19
+ multi_json (~> 1.4)
20
+ nokogiri (~> 1.5.2)
21
+ oauth2
22
+ hashie (2.0.5)
23
+ highline (1.6.20)
24
+ httpauth (0.2.0)
25
+ jeweler (1.8.8)
26
+ builder
27
+ bundler (~> 1.0)
28
+ git (>= 1.2.5)
29
+ github_api (= 0.10.1)
30
+ highline (>= 1.6.15)
31
+ nokogiri (= 1.5.10)
32
+ rake
33
+ rdoc
34
+ json (1.8.1)
35
+ jwt (0.1.8)
36
+ multi_json (>= 1.5)
37
+ method_source (0.8.2)
38
+ mock_redis (0.10.0)
39
+ multi_json (1.8.2)
40
+ multi_xml (0.5.5)
41
+ multipart-post (1.2.0)
42
+ nokogiri (1.5.10)
43
+ oauth2 (0.9.2)
44
+ faraday (~> 0.8)
45
+ httpauth (~> 0.2)
46
+ jwt (~> 0.1.4)
47
+ multi_json (~> 1.0)
48
+ multi_xml (~> 0.5)
49
+ rack (~> 1.2)
50
+ pry (0.9.12.4)
51
+ coderay (~> 1.0)
52
+ method_source (~> 0.8)
53
+ slop (~> 3.4)
54
+ rack (1.5.2)
55
+ rake (10.1.0)
56
+ rdoc (3.12.2)
57
+ json (~> 1.4)
58
+ rspec (2.8.0)
59
+ rspec-core (~> 2.8.0)
60
+ rspec-expectations (~> 2.8.0)
61
+ rspec-mocks (~> 2.8.0)
62
+ rspec-core (2.8.0)
63
+ rspec-expectations (2.8.0)
64
+ diff-lcs (~> 1.1.2)
65
+ rspec-mocks (2.8.0)
66
+ simplecov (0.8.2)
67
+ docile (~> 1.1.0)
68
+ multi_json
69
+ simplecov-html (~> 0.8.0)
70
+ simplecov-html (0.8.0)
71
+ slop (3.4.7)
72
+ sqlite3 (1.3.8)
73
+
74
+ PLATFORMS
75
+ ruby
76
+
77
+ DEPENDENCIES
78
+ activerecord
79
+ jeweler (~> 1.8.4)
80
+ mock_redis (~> 0.10.0)
81
+ pry (~> 0.9.12.4)
82
+ rdoc (~> 3.12)
83
+ rspec (~> 2.8.0)
84
+ simplecov (~> 0.8.2)
85
+ sqlite3 (~> 1.3.8)
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 David Huie
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,8 @@
1
+ # stacks
2
+
3
+ Fancy redis-backed caches.
4
+
5
+ ## Copyright
6
+
7
+ Copyright (c) 2013 NationBuilder. See LICENSE.txt for
8
+ further details.
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ begin
5
+ Bundler.setup(:default, :development)
6
+ rescue Bundler::BundlerError => e
7
+ $stderr.puts e.message
8
+ $stderr.puts "Run `bundle install` to install missing gems"
9
+ exit e.status_code
10
+ end
11
+
12
+ require 'rake'
13
+ require 'jeweler'
14
+
15
+ Jeweler::Tasks.new do |gem|
16
+ gem.name = "stacks"
17
+ gem.homepage = "http://github.com/3dna/stacks"
18
+ gem.license = "MIT"
19
+ gem.summary = 'Fancy redis-backed caches'
20
+ gem.description = 'Fancy redis-backed caches'
21
+ gem.email = "david@nationbuilder.com"
22
+ gem.authors = ["David Huie"]
23
+ end
24
+
25
+ Jeweler::RubygemsDotOrgTasks.new
26
+
27
+ require 'rspec/core'
28
+ require 'rspec/core/rake_task'
29
+
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ task :default => :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,48 @@
1
+ require 'digest/sha2'
2
+ require 'active_record'
3
+ require 'set'
4
+
5
+ class Stacks
6
+
7
+ class << self
8
+ attr_accessor(:extra_prefix,
9
+ :redis,
10
+ :redis_prefix,
11
+ :key_separator,
12
+ :model_listening_caches,
13
+ :deactivate,
14
+ :restrict)
15
+ end
16
+
17
+ self.redis_prefix = 'stacks'
18
+ self.key_separator = ':'
19
+
20
+ module Backends; end
21
+ module Items; end
22
+
23
+ class NoValueException < Exception; end
24
+
25
+ def self.bust_all_caches
26
+ keys = redis.keys("#{redis_prefix}*")
27
+
28
+ redis.pipelined do
29
+ keys.each { |key| redis.del(key) }
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ require 'stacks/backends/backend'
36
+ require 'stacks/backends/key_value_backend'
37
+ require 'stacks/backends/namespaced_backend'
38
+ require 'stacks/items/column_dependent_block'
39
+ require 'stacks/items/method_call'
40
+ require 'stacks/items/proc'
41
+ require 'stacks/items/timestamp'
42
+ require 'stacks/cache'
43
+ require 'stacks/method_cache'
44
+ require 'stacks/column_dependent_cache'
45
+ require 'stacks/report_cache'
46
+ require 'stacks/model_extensions'
47
+
48
+ Stacks.model_listening_caches = [Stacks::ColumnDependentCache]
@@ -0,0 +1,38 @@
1
+ module Stacks::Backends::Backend
2
+
3
+ def prefix_keys
4
+ keys = [Stacks.redis_prefix, backend_key]
5
+ keys << Stacks.extra_prefix.call if Stacks.extra_prefix
6
+ keys
7
+ end
8
+
9
+ def prefix_key
10
+ prefix_keys.join(Stacks::key_separator)
11
+ end
12
+
13
+ def suffix_key(item)
14
+ item.key
15
+ end
16
+
17
+ def key(item)
18
+ [prefix_key, suffix_key(item)].join(Stacks::key_separator)
19
+ end
20
+
21
+ def fill(item, ttl)
22
+ value = set(item)
23
+ expire(item, ttl)
24
+ value
25
+ end
26
+
27
+ def get_or_set(item, ttl)
28
+ begin
29
+ value = get(item)
30
+ return value
31
+ rescue Stacks::NoValueException
32
+
33
+ end
34
+
35
+ fill(item, ttl)
36
+ end
37
+
38
+ end
@@ -0,0 +1,29 @@
1
+ class Stacks::Backends::KeyValueBackend
2
+
3
+ include Stacks::Backends::Backend
4
+
5
+ def backend_key
6
+ "kv"
7
+ end
8
+
9
+ def get(item)
10
+ potential_value = Stacks.redis.get(key(item))
11
+ raise Stacks::NoValueException unless potential_value
12
+ Marshal.load(potential_value) if potential_value
13
+ end
14
+
15
+ def set(item)
16
+ value = item.value
17
+ Stacks.redis.set(key(item), Marshal.dump(value))
18
+ value
19
+ end
20
+
21
+ def del(item)
22
+ Stacks.redis.del(key(item))
23
+ end
24
+
25
+ def expire(item, ttl)
26
+ Stacks.redis.expire(key(item), ttl)
27
+ end
28
+
29
+ end
@@ -0,0 +1,47 @@
1
+ class Stacks::Backends::NamespacedBackend
2
+
3
+ include Stacks::Backends::Backend
4
+
5
+ attr_accessor :namespace
6
+
7
+ def backend_key
8
+ "n"
9
+ end
10
+
11
+ def prefix_keys
12
+ super << @namespace
13
+ end
14
+
15
+ def get(item)
16
+ potential_value = Stacks.redis.hget(prefix_key, suffix_key(item))
17
+ raise Stacks::NoValueException unless potential_value
18
+ return Marshal.load(potential_value) if potential_value
19
+ end
20
+
21
+ def set(item)
22
+ value = item.value
23
+ Stacks.redis.hset(prefix_key, suffix_key(item), Marshal.dump(value))
24
+ value
25
+ end
26
+
27
+ def expire(item, ttl)
28
+ Stacks.redis.expire(prefix_key, ttl)
29
+ end
30
+
31
+ def clear_cache
32
+ Stacks.redis.del(prefix_key)
33
+ end
34
+
35
+ def keys
36
+ Stacks.redis.hkeys(prefix_key)
37
+ end
38
+
39
+ def del_key(key)
40
+ Stacks.redis.hdel(prefix_key, key)
41
+ end
42
+
43
+ def del(item)
44
+ del_key(suffix_key(item))
45
+ end
46
+
47
+ end
@@ -0,0 +1,13 @@
1
+ module Stacks::Cache
2
+
3
+ def get_value(item, backend, ttl)
4
+ return item.value if Stacks.deactivate
5
+
6
+ if Stacks.restrict
7
+ return item.value unless Stacks.restrict.call
8
+ end
9
+
10
+ backend.get_or_set(item, ttl)
11
+ end
12
+
13
+ end
@@ -0,0 +1,98 @@
1
+ class Stacks::ColumnDependentCache
2
+
3
+ extend Stacks::Cache
4
+
5
+ class << self
6
+ attr_accessor :default_ttl, :cache_filling_job
7
+ end
8
+
9
+ # Expire everything after one week
10
+ self.default_ttl = 60*60*24*7
11
+
12
+ class InvalidModel < Exception; end
13
+
14
+ def self.validate_model(model)
15
+ raise InvalidModel.new(model.to_s) unless model < ActiveRecord::Base
16
+ end
17
+
18
+ def self.get_backend(model)
19
+ backend = Stacks::Backends::NamespacedBackend.new
20
+ backend.namespace = model.to_s
21
+ backend
22
+ end
23
+
24
+ def self.defined_blocks
25
+ @defined_caches ||= {}
26
+ end
27
+
28
+ def self.fillable_block_keys
29
+ @fillable_block_keys ||= Hash.new { |h, k| h[k] = {} }
30
+ end
31
+
32
+ def self.get_block_value(identifier)
33
+ item = defined_blocks[identifier]
34
+ backend = get_backend(item.model)
35
+
36
+ get_value(item, backend, default_ttl)
37
+ end
38
+
39
+ def self.define_block(model, columns, identifier, &block)
40
+ item = get_item(model, columns, identifier, block)
41
+ defined_blocks[identifier] = item
42
+ fillable_block_keys[model][item.key] = item
43
+ end
44
+
45
+ def self.make_fill_decision(model, key)
46
+ if cache_filling_job
47
+ cache_filling_job.call(model, key)
48
+ else
49
+ fill_block(model, key)
50
+ end
51
+ end
52
+
53
+ def self.fill_block(model, key)
54
+ item = fillable_block_keys[model][key]
55
+ backend = get_backend(item.model)
56
+ backend.fill(item, default_ttl)
57
+ end
58
+
59
+ def self.get_item(model, columns, identifier, block)
60
+ validate_model(model)
61
+ columns = [columns] unless columns.is_a?(Array)
62
+ Stacks::Items::ColumnDependentBlock.new(model, columns, identifier, block)
63
+ end
64
+
65
+ def self.cached(model, columns, identifier, &block)
66
+ backend = get_backend(model)
67
+ item = get_item(model, columns, identifier, block)
68
+
69
+ get_value(item, backend, default_ttl)
70
+ end
71
+
72
+ def self.bust_cache(model, columns)
73
+ backend = get_backend(model)
74
+ columns = columns.map { |c| c.to_s }
75
+
76
+ fill_keys = Set.new
77
+ del_keys = Set.new
78
+
79
+ backend.keys.each do |key|
80
+ potential_columns = Stacks::Items::ColumnDependentBlock.key_to_columns(key)
81
+
82
+ columns.each do |column|
83
+ if potential_columns.include?(column)
84
+ if fillable_block_keys[model].keys.include?(key)
85
+ fill_keys << [model, key]
86
+ next
87
+ end
88
+
89
+ del_keys << key
90
+ end
91
+ end
92
+ end
93
+
94
+ fill_keys.each { |model, key| make_fill_decision(model, key) }
95
+ del_keys.each { |key| backend.del_key(key) }
96
+ end
97
+
98
+ end