perforated 0.8.2 → 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 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