perforated 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 88f1b011129d5fe9d05a47fbbfde518bcbdc271a
4
- data.tar.gz: c8d932fadb905e4b95c7daaff47e7c6e2a406dee
3
+ metadata.gz: af43ff72681ac3aa03057419afb02db11d89dcec
4
+ data.tar.gz: 52f9abfc9c9239e34fbde54958e22f8bdbf3d9e7
5
5
  SHA512:
6
- metadata.gz: 21f8ba0179ca2b0a8748e5dc565017878c900fb4eff8b84e3b91960d9112cbbf55e8ae6b76f8c03bb1211e573b76c401be6a926170c6e9a246dcd5c7e247ea1d
7
- data.tar.gz: 7633062dd1634963e018fff974059fb373614a396b088cda8bac13ed2ba1defe6e6d478e21ac12835828f1f3480fc54d46855f1c267afec8c66f85aae7ba2900
6
+ metadata.gz: 4c84edc00c26382975abd7a028687c54e9cc2f99c1717324f95a60aa2fc3bb9471acff847a1b65cead2aaa62a03509c4af0a95f7083b6c4eecf49d46b201cec3
7
+ data.tar.gz: 01eba26e7cb2e63d9ac66cedebed88b2a2b7943aacbf9a928d3bfef8d6ebeaed53db168f748581cececc235c5f51643d86e1aac845c3a71ff9275ccd0204da8e
data/.gitignore CHANGED
@@ -17,3 +17,4 @@ test/version_tmp
17
17
  tmp
18
18
  vendor
19
19
  bin
20
+ TODO
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## Version 0.9.0
2
+
3
+ * `as_json` and `to_json` now take a block. The block will be applied to each
4
+ model as it is being cached, allowing for serializers or presenters without
5
+ much additional memory overhead.
6
+ * `as_json` has been removed. The overhead from marshalling/unmarshalling had
7
+ enough overhead that it negated the caching benefits.
8
+ * Strategies no longer expect a `suffix` property, as anything cached is
9
+ expected to be a JSON string.
10
+ * Caching is performed in batches, which can be controlled by passing
11
+ `batch_size` through to `to_json`. Arrays are refined to support
12
+ `find_in_batches` in order to maintain compatibility with ActiveRecord
13
+ Relations. For especially large collections this can provide significant
14
+ memory savings (particularly when paired with passing a block through for
15
+ custom serialization).
16
+
1
17
  ## Version 0.8.2
2
18
 
3
19
  * Really force the use of custom `fetch_multi` when using `NullStore`, not just
data/Gemfile CHANGED
@@ -2,6 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'dalli'
5
+ gem 'oj'
6
6
  gem 'redis'
7
- gem 'redis-activesupport'
7
+ gem 'redis-activesupport', github: 'redis-store/redis-activesupport'
data/README.md CHANGED
@@ -117,12 +117,12 @@ cache method you can provide a custom key caching strategy.
117
117
 
118
118
  ```ruby
119
119
  module CustomStrategy
120
- def self.expand_cache_key(object, suffix)
121
- [object.id, object.updated_at, suffix].join('/')
120
+ def self.expand_cache_key(object)
121
+ [object.id, object.updated_at].join('/')
122
122
  end
123
123
  end
124
124
 
125
- perforated = Perforated::Cache.new(array, CustomStrategy)
125
+ perforated = Perforated.new(array, CustomStrategy)
126
126
  ```
127
127
 
128
128
  ## Installation
data/bench/giant.rb CHANGED
@@ -3,10 +3,8 @@ require 'bundler'
3
3
  Bundler.setup
4
4
 
5
5
  require 'benchmark'
6
- require 'dalli'
7
6
  require 'perforated'
8
7
  require 'active_support/core_ext/object'
9
- require 'active_support/cache/dalli_store'
10
8
  require 'redis'
11
9
  require 'redis-activesupport'
12
10
 
@@ -17,7 +15,7 @@ Structure = Struct.new(:id) do
17
15
  end
18
16
 
19
17
  module Strategy
20
- def self.expand_cache_key(object, suffix)
18
+ def self.expand_cache_key(object)
21
19
  "perf-#{object}"
22
20
  end
23
21
  end
@@ -25,24 +23,26 @@ end
25
23
  perforated = Perforated::Cache.new((0..20_000).map { |i| Structure.new(i) }, Strategy)
26
24
 
27
25
  Benchmark.bm do |x|
26
+ GC.disable
27
+ puts "Total Objects: #{ObjectSpace.count_objects[:TOTAL]}"
28
+
28
29
  x.report('memory-1') { perforated.to_json }
29
30
  x.report('memory-2') { perforated.to_json }
30
31
 
32
+ puts "Total Objects: #{ObjectSpace.count_objects[:TOTAL]}"
33
+
31
34
  Perforated.configure do |config|
32
35
  config.cache = ActiveSupport::Cache::RedisStore.new(host: 'localhost', db: 5)
33
36
  end
34
37
 
35
38
  Perforated.cache.clear
36
39
 
40
+ GC.enable
41
+ GC.start
42
+ GC.disable
43
+
37
44
  x.report('redis-1') { perforated.to_json }
38
45
  x.report('redis-2') { perforated.to_json }
39
46
 
40
- Perforated.configure do |config|
41
- config.cache = ActiveSupport::Cache::DalliStore.new('localhost')
42
- end
43
-
44
- Perforated.cache.clear
45
-
46
- x.report('dalli-1') { perforated.to_json }
47
- x.report('dalli-2') { perforated.to_json }
47
+ puts "Total Objects: #{ObjectSpace.count_objects[:TOTAL]}"
48
48
  end
@@ -0,0 +1,23 @@
1
+ require 'bundler'
2
+
3
+ Bundler.setup
4
+
5
+ require 'benchmark'
6
+ require 'perforated'
7
+ require 'oj'
8
+
9
+ keys = %w[notes tags comments]
10
+ objects = (0..10_000).map do |num|
11
+ keys = keys.rotate
12
+
13
+ { keys.first => num }
14
+ end
15
+
16
+ strings = objects.map { |obj| JSON.dump(obj) }
17
+ rebuilder = Perforated::Rebuilder.new(strings, Oj)
18
+
19
+ N = 100
20
+
21
+ Benchmark.bmbm do |x|
22
+ x.report('rebuild') { N.times { rebuilder.rebuild(rooted: true) } }
23
+ end
data/lib/perforated.rb CHANGED
@@ -1,16 +1,12 @@
1
1
  require 'active_support/cache'
2
2
  require 'json'
3
3
  require 'perforated/cache'
4
- require 'perforated/compatibility/fetch_multi'
5
- require 'perforated/rooted'
6
- require 'perforated/strategy/default'
7
- require 'perforated/version'
8
4
 
9
5
  module Perforated
10
6
  extend self
11
7
 
12
8
  def new(*args)
13
- Perforated::Cache.new(args)
9
+ Perforated::Cache.new(*args)
14
10
  end
15
11
 
16
12
  def cache=(new_cache)
@@ -1,40 +1,42 @@
1
+ require 'perforated/rebuilder'
2
+ require 'perforated/strategy'
3
+ require 'perforated/compatibility/find_in_batches'
4
+ require 'perforated/compatibility/fetch_multi'
5
+
1
6
  module Perforated
2
7
  class Cache
3
- attr_reader :enumerable, :key_strategy
4
-
5
- def initialize(enumerable, key_strategy = Perforated::Strategy::Default)
6
- @enumerable = enumerable
7
- @key_strategy = key_strategy
8
- end
8
+ using Perforated::Compatibility::FindInBatches
9
9
 
10
- def as_json(options = {})
11
- keyed = keyed_enumerable('as-json')
12
- objects = fetch_multi(keyed) { |key| keyed[key].as_json }.values
10
+ attr_accessor :enumerable, :strategy
13
11
 
14
- if options[:rooted]
15
- Perforated::Rooted.merge(objects)
16
- else
17
- objects
18
- end
12
+ def initialize(enumerable, strategy = Perforated::Strategy)
13
+ @enumerable = enumerable
14
+ @strategy = strategy
19
15
  end
20
16
 
21
- def to_json(options = {})
22
- keyed = keyed_enumerable('to-json')
23
- objects = fetch_multi(keyed) { |key| keyed[key].to_json }
24
- concat = concatenate(objects)
17
+ def to_json(rooted: false, batch_size: 1000, &block)
18
+ results = []
25
19
 
26
- if options[:rooted]
27
- Perforated::Rooted.reconstruct(concat)
28
- else
29
- concat
20
+ enumerable.find_in_batches(batch_size: batch_size) do |subset|
21
+ keyed = key_mapped(subset)
22
+
23
+ results << fetch_multi(keyed) do |key|
24
+ if block_given?
25
+ (yield keyed[key]).to_json
26
+ else
27
+ keyed[key].to_json
28
+ end
29
+ end.values
30
30
  end
31
+
32
+ Perforated::Rebuilder.new(results).rebuild(rooted: rooted)
31
33
  end
32
34
 
33
35
  private
34
36
 
35
- def keyed_enumerable(suffix = '')
36
- enumerable.each_with_object({}) do |object, memo|
37
- memo[key_strategy.expand_cache_key(object, suffix)] = object
37
+ def key_mapped(subset)
38
+ subset.each_with_object({}) do |object, memo|
39
+ memo[strategy.expand_cache_key(object)] = object
38
40
  end
39
41
  end
40
42
 
@@ -43,9 +45,5 @@ module Perforated
43
45
 
44
46
  Perforated::Compatibility.fetch_multi(*keys, &block)
45
47
  end
46
-
47
- def concatenate(objects)
48
- "[#{objects.values.join(',')}]"
49
- end
50
48
  end
51
49
  end
@@ -0,0 +1,11 @@
1
+ module Perforated
2
+ module Compatibility
3
+ module FindInBatches
4
+ refine Array do
5
+ def find_in_batches(batch_size: 1000, &block)
6
+ each_slice(batch_size, &block)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,50 @@
1
+ require 'set'
2
+
3
+ module Perforated
4
+ class Rebuilder
5
+ attr_reader :parser, :strings
6
+
7
+ def initialize(strings, parser = Perforated.json)
8
+ @strings = strings
9
+ @parser = parser
10
+ end
11
+
12
+ def rebuild(rooted: false)
13
+ if rooted
14
+ parser.dump(merge(parser.load(concatenated)))
15
+ else
16
+ concatenated
17
+ end
18
+ end
19
+
20
+ def concatenated
21
+ "[#{strings.join(',')}]"
22
+ end
23
+
24
+ private
25
+
26
+ def merge(objects)
27
+ merged = objects.each_with_object({}) do |object, memo|
28
+ object.each do |key, value|
29
+ memo[key] ||= Set.new
30
+
31
+ if value.is_a?(Array)
32
+ memo[key].merge(value)
33
+ else
34
+ memo[key].add(value)
35
+ end
36
+ end
37
+ end
38
+
39
+ sets_to_arrays(merged)
40
+ end
41
+
42
+ def sets_to_arrays(merged)
43
+ merged.each do |key, value|
44
+ merged[key] = value.to_a
45
+ end
46
+
47
+ merged
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ require 'active_support/cache'
2
+
3
+ module Perforated
4
+ module Strategy
5
+ def self.expand_cache_key(object)
6
+ ActiveSupport::Cache.expand_cache_key(object.cache_key)
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module Perforated
2
- VERSION = '0.8.2'
2
+ VERSION = '0.9.0'
3
3
  end
data/perforated.gemspec CHANGED
@@ -26,5 +26,5 @@ Gem::Specification.new do |spec|
26
26
 
27
27
  spec.add_development_dependency 'bundler'
28
28
  spec.add_development_dependency 'rake'
29
- spec.add_development_dependency 'rspec', '~> 2.14'
29
+ spec.add_development_dependency 'rspec', '~> 3'
30
30
  end
@@ -4,12 +4,8 @@ describe Perforated::Cache do
4
4
  after { Perforated.cache.clear }
5
5
 
6
6
  Language = Struct.new(:name) do
7
- def as_json
8
- { name: name }
9
- end
10
-
11
7
  def to_json
12
- as_json.to_json
8
+ { name: name }.to_json
13
9
  end
14
10
 
15
11
  def cache_key
@@ -18,12 +14,8 @@ describe Perforated::Cache do
18
14
  end
19
15
 
20
16
  Family = Struct.new(:name, :languages) do
21
- def as_json
22
- { languages: languages }
23
- end
24
-
25
17
  def to_json
26
- as_json.to_json
18
+ { languages: languages }.to_json
27
19
  end
28
20
 
29
21
  def cache_key
@@ -31,54 +23,51 @@ describe Perforated::Cache do
31
23
  end
32
24
  end
33
25
 
34
- describe '#as_json' do
35
- it 'constructs automatically cached serialized output' do
26
+ describe '#to_json' do
27
+ it 'constructs a stringified json array of underlying values' do
36
28
  ruby = Language.new('Ruby')
37
29
  elixir = Language.new('Elixir')
38
30
  cache = Perforated::Cache.new([ruby, elixir])
39
31
 
40
- expect(cache.as_json).to eq([{ name: 'Ruby' }, { name: 'Elixir' }])
41
-
42
- expect(Perforated.cache.read('Language/Ruby/as-json')).to eq(ruby.as_json)
43
- expect(Perforated.cache.read('Language/Elixir/as-json')).to eq(elixir.as_json)
32
+ expect(cache.to_json).to eq(%([{"name":"Ruby"},{"name":"Elixir"}]))
33
+ expect(Perforated.cache.exist?('Language/Ruby')).to be_truthy
34
+ expect(Perforated.cache.exist?('Language/Elixir')).to be_truthy
44
35
  end
45
36
 
46
37
  it 'does not overwrite existing key values' do
47
38
  erlang = Language.new('Erlang')
48
- Perforated.cache.write('Language/Erlang/as-json', { name: 'Elixir' })
49
-
50
- Perforated::Cache.new([erlang]).as_json
51
-
52
- expect(Perforated.cache.read('Language/Erlang/as-json')).to eq(name: 'Elixir')
53
- end
39
+ Perforated.cache.write('Language/Erlang', JSON.dump(name: 'Elixir'))
54
40
 
55
- it 'merges objects comprised of rooted arrays' do
56
- lisps = Family.new('Lisp', ['scheme', 'clojure'])
57
- scripts = Family.new('Script', ['perl', 'ruby'])
58
- cache = Perforated::Cache.new([lisps, scripts])
41
+ Perforated::Cache.new([erlang]).to_json
59
42
 
60
- expect(cache.as_json(rooted: true)).to eq(
61
- languages: %w[scheme clojure perl ruby]
62
- )
43
+ expect(Perforated.cache.read('Language/Erlang')).to eq(JSON.dump(name: 'Elixir'))
63
44
  end
64
45
 
65
46
  it 'safely returns an empty enumerable when empty' do
66
47
  cache = Perforated::Cache.new([])
67
48
 
68
- expect(cache.as_json).to eq([])
69
- expect(cache.as_json(rooted: true)).to eq({})
49
+ expect(cache.to_json).to eq('[]')
50
+ expect(cache.to_json(rooted: true)).to eq('{}')
70
51
  end
71
- end
72
52
 
73
- describe '#to_json' do
74
- it 'constructs a stringified json array of underlying values' do
75
- cache = Perforated::Cache.new([Language.new('Ruby'), Language.new('Elixir')])
53
+ it 'applies a provided block to the object before caching' do
54
+ ruby = Language.new('Ruby')
55
+ cache = Perforated::Cache.new([ruby])
76
56
 
77
- expect(cache.to_json).to eq(%([{"name":"Ruby"},{"name":"Elixir"}]))
78
- expect(Perforated.cache.exist?('Language/Ruby/to-json')).to be_truthy
79
- expect(Perforated.cache.exist?('Language/Elixir/to-json')).to be_truthy
57
+ serializer = Struct.new(:lang) do
58
+ def to_json
59
+ { name: lang.name.upcase }.to_json
60
+ end
61
+ end
62
+
63
+ results = cache.to_json do |lang|
64
+ serializer.new(lang)
65
+ end
66
+
67
+ expect(results).to eq(JSON.dump([{ name: 'RUBY' }]))
80
68
  end
81
69
 
70
+
82
71
  it 'reconstructs rooted objects into a single merged object' do
83
72
  lisps = Family.new('Lisp', ['scheme', 'clojure'])
84
73
  scripts = Family.new('Script', ['perl', 'ruby'])
@@ -0,0 +1,25 @@
1
+ require 'perforated/compatibility/find_in_batches'
2
+
3
+ describe Perforated::Compatibility::FindInBatches do
4
+ FindBatches = Struct.new(:array) do
5
+ using Perforated::Compatibility::FindInBatches
6
+
7
+ def perform(&block)
8
+ array.find_in_batches(batch_size: 50, &block)
9
+ end
10
+ end
11
+
12
+ describe '#find_in_batches' do
13
+ it 'iterates over an enumerable in batches' do
14
+ enumerable = [1] * 100
15
+ eachable = FindBatches.new(enumerable)
16
+ results = []
17
+
18
+ eachable.perform do |arr|
19
+ results << arr.reduce(&:+)
20
+ end
21
+
22
+ expect(results).to eq([50, 50])
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+ require 'perforated/rebuilder'
3
+
4
+ describe Perforated::Rebuilder do
5
+ describe '#rebuild' do
6
+ it 'merges stringified json' do
7
+ string_a = JSON.dump(families: { name: 'lang' }, languages: [{ name: 'scheme' }])
8
+ string_b = JSON.dump(families: { name: 'lang' }, languages: [{ name: 'clojure' }])
9
+ string_c = JSON.dump(families: { name: 'lang' })
10
+
11
+ rooted = Perforated::Rebuilder.new([string_a, string_b], JSON)
12
+
13
+ expect(rooted.rebuild(rooted: true)).to eq(
14
+ '{"families":[{"name":"lang"}],"languages":[{"name":"scheme"},{"name":"clojure"}]}'
15
+ )
16
+ end
17
+ end
18
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perforated
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Parker Selbert
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-27 00:00:00.000000000 Z
11
+ date: 2014-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '2.14'
61
+ version: '3'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '2.14'
68
+ version: '3'
69
69
  description: Intellgent json collection caching
70
70
  email:
71
71
  - parker@sorentwo.com
@@ -82,16 +82,19 @@ files:
82
82
  - README.md
83
83
  - Rakefile
84
84
  - bench/giant.rb
85
+ - bench/rebuilder.rb
85
86
  - lib/perforated.rb
86
87
  - lib/perforated/cache.rb
87
88
  - lib/perforated/compatibility/fetch_multi.rb
88
- - lib/perforated/rooted.rb
89
- - lib/perforated/strategy/default.rb
89
+ - lib/perforated/compatibility/find_in_batches.rb
90
+ - lib/perforated/rebuilder.rb
91
+ - lib/perforated/strategy.rb
90
92
  - lib/perforated/version.rb
91
93
  - perforated.gemspec
92
94
  - spec/perforated/cache_spec.rb
93
95
  - spec/perforated/compatibility/fetch_multi_spec.rb
94
- - spec/perforated/rooted_spec.rb
96
+ - spec/perforated/compatibility/find_in_batches_spec.rb
97
+ - spec/perforated/rebuilder_spec.rb
95
98
  - spec/perforated_spec.rb
96
99
  - spec/spec_helper.rb
97
100
  homepage: https://github.com/sorentwo/perforated
@@ -124,6 +127,7 @@ summary: 'The most expensive part of serving a JSON request is converting the se
124
127
  test_files:
125
128
  - spec/perforated/cache_spec.rb
126
129
  - spec/perforated/compatibility/fetch_multi_spec.rb
127
- - spec/perforated/rooted_spec.rb
130
+ - spec/perforated/compatibility/find_in_batches_spec.rb
131
+ - spec/perforated/rebuilder_spec.rb
128
132
  - spec/perforated_spec.rb
129
133
  - spec/spec_helper.rb
@@ -1,33 +0,0 @@
1
- require 'set'
2
-
3
- module Perforated
4
- module Rooted
5
- def self.merge(objects)
6
- merged = objects.each_with_object({}) do |object, memo|
7
- object.each do |key, value|
8
- memo[key] ||= Set.new
9
-
10
- if value.is_a?(Array)
11
- memo[key].merge(value)
12
- else
13
- memo[key].add(value)
14
- end
15
- end
16
- end
17
-
18
- sets_to_arrays(merged)
19
- end
20
-
21
- def self.reconstruct(objects, parser = Perforated.json)
22
- parser.dump(merge(parser.load(objects)))
23
- end
24
-
25
- def self.sets_to_arrays(object)
26
- object.each do |key, value|
27
- object[key] = value.to_a
28
- end
29
-
30
- object
31
- end
32
- end
33
- end
@@ -1,11 +0,0 @@
1
- module Perforated
2
- module Strategy
3
- class Default
4
- def self.expand_cache_key(object, suffix = '')
5
- args = object.cache_key + [suffix]
6
-
7
- ActiveSupport::Cache.expand_cache_key(args)
8
- end
9
- end
10
- end
11
- end
@@ -1,26 +0,0 @@
1
- require 'perforated'
2
-
3
- describe Perforated::Rooted do
4
- describe '.merge' do
5
- it 'merges all nested objects' do
6
- obj_a = { languages: [{ name: 'scheme' }] }
7
- obj_b = { languages: [{ name: 'clojure' }] }
8
-
9
- expect(Perforated::Rooted.merge([obj_a, obj_b])).to eq(
10
- languages: [{ name: 'scheme' }, { name: 'clojure' }]
11
- )
12
- end
13
- end
14
-
15
- describe '.reconstruct' do
16
- it 'merges stringified json' do
17
- obj_a = JSON.dump({ languages: [{ name: 'scheme' }] })
18
- obj_b = JSON.dump({ languages: [{ name: 'clojure' }] })
19
- concat = "[#{obj_a},#{obj_b}]"
20
-
21
- expect(Perforated::Rooted.reconstruct(concat)).to eq(
22
- '{"languages":[{"name":"scheme"},{"name":"clojure"}]}'
23
- )
24
- end
25
- end
26
- end