stacks 0.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.
@@ -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