ractor-cache 0.2.0 → 0.3.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
  SHA256:
3
- metadata.gz: c2fb8ce98adad688d2ff0e150265154ba963c62a4c7cd88e87bb09ab366db535
4
- data.tar.gz: fda4eb8307bb638e87841f418895c4f57297c975349c9502026b3346ad570256
3
+ metadata.gz: 8ed3b5b4ab4307230b9dab1a21f269ac9d3bae8803f11bb209798df4de02e031
4
+ data.tar.gz: ed75a15f8035a23955d1e25ce9c88e32a540881a2322ba0267c0c98302d50010
5
5
  SHA512:
6
- metadata.gz: 38ad35d50bf189e0dc9f0da88f91f484664811824f1310e2e4a040f49b19d3f9f079c826d9667956d5d3ec4949e440583f7f38d74c6f6841af40a9f65b672453
7
- data.tar.gz: fa23e9e4cebd2891ba3bfdfe81e8d88fe693e2efd770e2f08cc1be576bb3a46ddae543562c95c760e917f39f895ba64bb5e039fff6a68ea9e4eccb690194a8db
6
+ metadata.gz: 63984280a9bc9d9530e86cc4d9c69987a4dbcc0128cf3525bf434006bbfc3b00515f5fb1e2aa65c85050cb37691dd666d7b39f1d9d05574078c29713501c8780
7
+ data.tar.gz: 4e9725ab3449785e3b87a594c13c4175a083cc5910bf72c5fece2bcdcd4da7ce3c4ac4aa222553c6efceb8525a9490173e0f32274fdee13e6c9dfaeb01a0f878
@@ -5,13 +5,16 @@ on: [pull_request]
5
5
  jobs:
6
6
  tests:
7
7
  name: >-
8
- Specs | ${{ matrix.ruby }}
8
+ Specs ${{ matrix.with_backports }} | ${{ matrix.ruby }}
9
9
  runs-on: ${{ matrix.os }}-latest
10
10
  strategy:
11
11
  fail-fast: false
12
12
  matrix:
13
13
  os: [ ubuntu ]
14
- ruby: [ 2.4, 2.5, 2.6, 2.7, head ]
14
+ ruby: [ 2.4, 2.5, 2.6, 2.7, 3.0, head ]
15
+ with_backports: [ null ]
16
+ include:
17
+ - { os: ubuntu, ruby: 2.7, with_backports: '(with Backports)' }
15
18
  steps:
16
19
  - name: checkout
17
20
  uses: actions/checkout@v2
@@ -19,6 +22,9 @@ jobs:
19
22
  uses: ruby/setup-ruby@v1
20
23
  with:
21
24
  ruby-version: ${{ matrix.ruby }}
25
+ - name: setup Env
26
+ if: matrix.with_backports
27
+ run: echo "B=true" >> $GITHUB_ENV
22
28
  - name: install dependencies
23
29
  run: bundle install --jobs 3 --retry 3
24
30
  - name: spec
@@ -3,6 +3,11 @@ AllCops:
3
3
  TargetRubyVersion: 2.4
4
4
  NewCops: enable
5
5
 
6
+ # Export
7
+
8
+ Lint/EmptyClass:
9
+ Enabled: false
10
+
6
11
  # For sure
7
12
 
8
13
  Layout/LineLength:
@@ -52,3 +57,16 @@ Style/AccessModifierDeclarations:
52
57
 
53
58
  Style/DocumentDynamicEvalDefinition:
54
59
  Enabled: false
60
+
61
+ Layout/EmptyLineAfterMagicComment:
62
+ Enabled: false # https://github.com/rubocop-hq/rubocop/issues/9327
63
+
64
+ Style/MutableConstant:
65
+ Enabled: false # https://github.com/rubocop-hq/rubocop/issues/9328
66
+
67
+ Lint/UselessAssignment:
68
+ Enabled: false # https://github.com/rubocop-hq/rubocop/issues/9330
69
+
70
+ Layout/HashAlignment:
71
+ Exclude:
72
+ - 'lib/ractor/cache/cached_method.rb'
data/Gemfile CHANGED
@@ -5,8 +5,8 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in ractor-cache.gemspec
6
6
  gemspec
7
7
 
8
- gem 'backports', git: 'https://github.com/marcandre/backports.git', branch: 'ractor'
9
- gem 'pry-byebug', require: false
10
- gem 'rake', require: false
11
- gem 'rspec', require: false
12
- gem 'rubocop', require: false
8
+ gem 'backports'
9
+ gem 'pry-byebug'
10
+ gem 'rake'
11
+ gem 'rspec'
12
+ gem 'rubocop'
data/README.md CHANGED
@@ -23,9 +23,11 @@ end
23
23
  ## Why?
24
24
 
25
25
  0) It's pretty
26
- 1) It makes your class `Ractor`-compatible
26
+ 1) Handles `nil` / `false` results
27
+ 2) Works even for frozen instances
28
+ 3) Works even for deeply frozen instances (`Ractor`-shareable).
27
29
 
28
- ## `Ractor`-compatible?
30
+ ## `Ractor`-shareable?
29
31
 
30
32
  Ractor is new in Ruby 3.0 and is awesome.
31
33
 
@@ -69,14 +71,9 @@ foo.long_calc # => `FrozenError`, @cache is frozen
69
71
 
70
72
  ## How to resolve this
71
73
 
72
- This gem will:
73
- 1) Use a mutable data structure like above, which means no issue for shallow freezing an instance
74
- 2) If an instance is deeply frozen, the gem will either insure things will keep working by applying any of the following strategies:
75
- - prebuild the cache,
76
- - not write to the cache,
77
- - (or maybe use a separate Ractor / `SharedHash`)
74
+ This gem will use associate a mutable data structure to the instance. Even if deeply-frozen it can still mutate the data structure. The data is Ractor-local, so it won't be shared and won't cause issues. Internally a `WeakMap` is used to make sure objects are still garbage collected as they should.
78
75
 
79
- Implementation details pexplained here](hacker_guide.md)
76
+ Implementation details [explained here](hacker_guide.md)
80
77
 
81
78
  ## Contributing
82
79
 
@@ -1,5 +1,7 @@
1
1
  ### Structure
2
2
 
3
+ (Code simplified: handling of `nil` / `false` not shown for simplicity)
4
+
3
5
  ```ruby
4
6
  using Ractor::Cache
5
7
 
@@ -17,7 +19,7 @@ class Mammal < Animal
17
19
  end
18
20
 
19
21
  class Ape < Mammal
20
- cache def something_else # Supported if same strategy and signature
22
+ cache def something_else
21
23
  return super unless a_predicate?
22
24
 
23
25
  # specialized calculation
@@ -26,7 +28,7 @@ class Ape < Mammal
26
28
  def complex(arg)
27
29
  # ... calculation
28
30
  def
29
- cache :complex, strategy: :disable
31
+ cache :complex
30
32
  end
31
33
  ```
32
34
 
@@ -34,42 +36,25 @@ Equivalent code:
34
36
  ```
35
37
  class Animal
36
38
  module RactorCacheLayer
37
- CACHED = ... # private information of how things are cached
38
-
39
39
  # Where caching info is stored for `Animal`
40
40
  class Store
41
- def initialize(owner)
42
- @owner = owner
41
+ def cached_something_or_init
42
+ @something ||= yield
43
43
  end
44
44
 
45
- attr_accessor :something, :something_else
46
-
47
- def freeze # only called in case of deep-freezing
48
- @owner.class::RactorCacheLayer.deep_freeze_callback(@owner)
49
- super
50
- end
45
+ # same for something_else
51
46
  end
52
47
 
53
48
  def something
54
- ractor_cache.something ||= super
55
- end
56
-
57
- def freeze
58
- ractor_cache # make sure cache store is built
59
- super
60
- end
61
-
62
- def self.deep_freeze_callback(instance)
63
- # strategy for `something` is prebuild:
64
- instance.something
65
- # same for `something_else`:
66
- instance.something_else
49
+ ractor_cache.cached_something_or_init { super }
67
50
  end
68
51
 
69
52
  private
70
53
 
71
54
  def ractor_cache
55
+ # similar to:
72
56
  @ractor_cache ||= self.class::Store.new
57
+ # but actually using a ractor-local WeakMap instead of an instance variable
73
58
  end
74
59
  end
75
60
  prepend CacheLayer
@@ -85,38 +70,23 @@ end
85
70
 
86
71
  class Ape < Mammal
87
72
  module RactorCacheLayer
88
- CACHED = ... # private array of Strategy
89
-
90
73
  # Where caching info is stored for `Ape`
91
74
  class Store < Animal::CacheLayer::Store
92
- attr_reader :complex
93
-
94
- def initialize(owner)
95
- @complex = {}
96
- super
75
+ def initilialize
76
+ @complex_fetch = {}
97
77
  end
98
- end
99
78
 
100
- def something_else
101
- ractor_cache.something_else ||= super
79
+ def cached_complex_or_init(owner)
80
+ @complex_fetch[owner] ||= yield
81
+ end
102
82
  end
103
83
 
104
84
  def complex(arg)
105
- hash = ractor_cache.complex
106
- hash.fetch(arg) do
107
- result = super
108
- # strategy is disable:
109
- hash[arg] = result unless frozen?
110
- result
111
- end
85
+ ractor_cache.cached_complex_or_init(arg) { super }
112
86
  end
113
87
 
114
- def self.deep_freeze_callback(instance)
115
- # strategy for `complex` is disable
116
- # nothing to do
117
- # process any other cached data
118
- # and then
119
- super
88
+ def something_else # refine again
89
+ ractor_cache.cached_something_else_or_init { super }
120
90
  end
121
91
  end
122
92
  prepend CacheLayer
@@ -134,10 +104,13 @@ Ape.ancestors # =>
134
104
  Animal,
135
105
  Object, #...
136
106
  ]
137
- ```
138
-
139
- ### Class ancestors
140
107
 
141
- In the "equivalent" code above, `Animal::CacheLayer` is actually an instance of `Ractor::Cache::CacheLayer`.
142
-
143
- `Animal::CacheLayer::Store` is a subclass of `Ractor::Cache::Store`
108
+ m = Ape.instance_method(:something_else)
109
+ # => #<UnboundMethod: Ape::CacheLayer#something_else()>
110
+ m = m.super_method
111
+ # => #<UnboundMethod: Ape#something_else()>
112
+ m = m.super_method
113
+ # => #<UnboundMethod: Animal::CacheLayer#something_else()>
114
+ m = m.super_method
115
+ # => #<UnboundMethod: Animal#something_else()>
116
+ ```
@@ -7,8 +7,8 @@ class Ractor
7
7
  module Cache
8
8
  require_relative_dir
9
9
 
10
- private def cache(method_name, strategy: nil)
11
- CachingLayer[self].cache(method_name, strategy: strategy)
10
+ private def cache(method_name)
11
+ CachingLayer[self].cache(method_name)
12
12
  end
13
13
 
14
14
  refine Module do
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_values: literal
3
+
4
+ class Ractor
5
+ module Cache
6
+ module CachedMethod
7
+ Base = Struct.new(:method_name, :argument_kind)
8
+ class Base
9
+ def compile_method
10
+ <<~RUBY
11
+ def #{method_name}#{args}
12
+ ractor_cache.cached_#{method_name}_or_init#{args} { super }
13
+ end
14
+ RUBY
15
+ end
16
+
17
+ SIGNATURES = {
18
+ none: [''],
19
+ positional: ['(*args)', 'args'],
20
+ keyword: ['(**opt)', 'opt'],
21
+ both: ['(*args, **opt)', '[args, opt]'],
22
+ }
23
+ private_constant :SIGNATURES
24
+
25
+ private def args
26
+ SIGNATURES.fetch(argument_kind).first
27
+ end
28
+
29
+ private def key
30
+ SIGNATURES.fetch(argument_kind).fetch(1)
31
+ end
32
+ end
33
+
34
+ class WithoutArgument < Base
35
+ def compile_initializer
36
+ ''
37
+ end
38
+
39
+ def compile_accessor
40
+ <<~RUBY
41
+ def cached_#{method_name}_or_init
42
+ return @#{method_name} if defined?(@#{method_name})
43
+
44
+ @#{method_name} = yield
45
+ end
46
+ RUBY
47
+ end
48
+ end
49
+
50
+ class WithArguments < Base
51
+ def compile_initializer
52
+ "@#{method_name} = {}"
53
+ end
54
+
55
+ def compile_accessor
56
+ <<~RUBY
57
+ def cached_#{method_name}_or_init#{args}
58
+ @#{method_name}.fetch(#{key}) do
59
+ @#{method_name}[#{key}] = yield
60
+ end
61
+ end
62
+ RUBY
63
+ end
64
+ end
65
+
66
+ class << self
67
+ def new(instance_method)
68
+ kind = argument_kind(instance_method.parameters)
69
+ klass = kind == :none ? WithoutArgument : WithArguments
70
+ klass.new(instance_method.name, kind)
71
+ end
72
+
73
+ private def argument_kind(parameters) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
74
+ kind = :none
75
+ parameters.each do |type, name|
76
+ case type
77
+ when :req, :opt, :rest
78
+ return :both if type == :rest && name == :*
79
+
80
+ kind = :positional
81
+ when :key, :keyrest
82
+ return kind == :positional ? :both : :keyword
83
+ when :nokey, :block
84
+ # ignore
85
+ end
86
+ end
87
+ kind
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -3,18 +3,57 @@
3
3
  class Ractor
4
4
  module Cache
5
5
  module CachingLayer
6
- def freeze
7
- ractor_cache # Make sure the instance variable is created beforehand
8
- super
9
- end
6
+ use_ractor_storage = defined?(Ractor.current) && # check we are not running Backports on a Ruby that is compatible:
7
+ begin
8
+ (::ObjectSpace::WeakMap.new[Object.new.freeze] = []) # Ruby 2.6- didn't work with frozen keys
9
+ rescue FrozenError
10
+ false
11
+ end
10
12
 
11
- private def ractor_cache
12
- @ractor_cache ||= self.class::Store.new(self)
13
+ if use_ractor_storage
14
+ private def ractor_cache
15
+ CachingLayer.ractor_storage[self] ||= self.class::Store.new
16
+ end
17
+ else
18
+ def freeze
19
+ ractor_cache
20
+ super
21
+ end
22
+
23
+ private def ractor_cache
24
+ @ractor_cache ||= self.class::Store.new
25
+ end
13
26
  end
14
27
 
15
28
  class << self
29
+ def ractor_storage
30
+ Ractor.current[:RactorCacheLocalStore] ||= ::ObjectSpace::WeakMap.new
31
+ end
32
+
16
33
  attr_reader :sublayer, :cached, :parent
17
34
 
35
+ def cache(method_name)
36
+ cm = cached_method(method_name)
37
+
38
+ file, line = cm.method(:compile_method).source_location
39
+ module_eval(cm.compile_method, file, line + 2)
40
+ @cached << cm
41
+ update_store_methods(self::Store)
42
+ end
43
+
44
+ private def update_store_methods(store)
45
+ init = @cached.map(&:compile_initializer).join("\n")
46
+
47
+ store.class_eval <<~RUBY, __FILE__, __LINE__ + 1
48
+ def initialize
49
+ #{init}
50
+ end
51
+
52
+ #{@cached.last.compile_accessor}
53
+ RUBY
54
+ end
55
+
56
+ # @api private
18
57
  def attach(mod, sublayer)
19
58
  @sublayer = sublayer
20
59
  @cached = []
@@ -26,29 +65,14 @@ class Ractor
26
65
  self
27
66
  end
28
67
 
29
- def cache(method, strategy:)
30
- strat = build_stragegy(method, strategy)
31
- file, line = strat.method(:compile_accessor).source_location
32
- module_eval(strat.compile_accessor, file, line + 2)
33
- @cached << strat
34
- self::Store.update(@cached)
35
- end
36
-
37
- def deep_freeze_callback(instance)
38
- @cached.each do |strategy|
39
- strategy.deep_freeze_callback(instance)
40
- end
41
- sublayer&.deep_freeze_callback(instance)
42
- end
43
-
44
- # @returns [Strategy]
45
- private def build_stragegy(method, strategy)
68
+ # @returns [CachedMethod]
69
+ private def cached_method(method_name)
46
70
  im = begin
47
- @parent.instance_method(method)
71
+ @parent.instance_method(method_name)
48
72
  rescue ::NameError => e
49
73
  raise e, "#{e.message}. Method must be defined before calling `cache`", e.backtrace
50
74
  end
51
- Strategy.new(strategy, to_cache: im)
75
+ CachedMethod.new(im)
52
76
  end
53
77
 
54
78
  # Return `CachingLayer` in `mod`, creating it if need be.
@@ -3,35 +3,7 @@
3
3
  class Ractor
4
4
  module Cache
5
5
  class Store
6
- def initialize(owner) # Possibly redefined by `update`
7
- @owner = owner
8
- end
9
-
10
- def freeze
11
- @owner.class::RactorCacheLayer.deep_freeze_callback(@owner)
12
- super
13
- end
14
-
15
- class << self
16
- def update(cached)
17
- update_accessors(cached)
18
- update_init(cached)
19
- end
20
-
21
- private def update_accessors(cached)
22
- attr_accessor(*cached.map(&:method_name))
23
- end
24
-
25
- private def update_init(cached)
26
- body = cached.map(&:compile_store_init).join("\n")
27
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
28
- def initialize(owner)
29
- #{body}
30
- super
31
- end
32
- RUBY
33
- end
34
- end
6
+ # No base methods. All actual methods defined by `CachingLayer`
35
7
  end
36
8
  end
37
9
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  class Ractor
4
4
  module Cache
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ractor-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-Andre Lafortune
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-11 00:00:00.000000000 Z
11
+ date: 2021-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: require_relative_dir
@@ -44,9 +44,9 @@ files:
44
44
  - bin/setup
45
45
  - hacker_guide.md
46
46
  - lib/ractor/cache.rb
47
+ - lib/ractor/cache/cached_method.rb
47
48
  - lib/ractor/cache/caching_layer.rb
48
49
  - lib/ractor/cache/store.rb
49
- - lib/ractor/cache/strategy.rb
50
50
  - lib/ractor/cache/version.rb
51
51
  - ractor-cache.gemspec
52
52
  homepage: https://github.com/marcandre/ractor-cache
@@ -1,121 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Ractor
4
- module Cache
5
- EMPTY_CACHE = Class.new.freeze # lazy way to get a name
6
-
7
- module Strategy
8
- class Base
9
- attr_reader :parameters, :method_name
10
-
11
- def initialize(instance_method)
12
- @method_name = instance_method.name
13
- analyse_parameters(instance_method.parameters)
14
- end
15
-
16
- def compile_store_init
17
- init = @has_arguments ? "Hash.new { #{EMPTY_CACHE} }" : EMPTY_CACHE
18
- "@#{method_name} = #{init}"
19
- end
20
-
21
- private def analyse_parameters(parameters) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
22
- @has_positional_arguments = @has_keywords_arguments = false
23
- parameters.each do |type, name|
24
- case type
25
- when :req, :opt, :rest
26
- @has_positional_arguments = true
27
- @has_keywords_arguments = true if type == :rest && name == :*
28
- when :key, :keyrest
29
- @has_keywords_arguments = true
30
- when :nokey, :block
31
- # ignore
32
- end
33
- @has_arguments = @has_positional_arguments || @has_keywords_arguments
34
- end
35
- end
36
-
37
- private def compile_lookup
38
- args = [
39
- *('args' if @has_positional_arguments),
40
- *('opts' if @has_keywords_arguments),
41
- ]
42
-
43
- return if args.empty?
44
-
45
- "[#{args.join(', ')}]"
46
- end
47
-
48
- private def signature
49
- [
50
- *('*args' if @has_positional_arguments),
51
- *('**opts' if @has_keywords_arguments),
52
- ].join(', ')
53
- end
54
- end
55
-
56
- class Prebuild < Base
57
- def initialize(*)
58
- super
59
- raise ArgumentError, "Can not cache method #{method_name} by prebuilding because it accepts arguments" if @has_arguments
60
- end
61
-
62
- def deep_freeze_callback(instance)
63
- instance.__send__ method_name
64
- end
65
-
66
- def compile_accessor
67
- <<~RUBY
68
- def #{method_name}(#{signature})
69
- r = ractor_cache.#{method_name}
70
- return r unless r == EMPTY_CACHE
71
-
72
- ractor_cache.#{method_name} = super
73
- end
74
- RUBY
75
- end
76
- end
77
-
78
- class Disable < Base
79
- def compile_accessor
80
- <<~RUBY
81
- def #{method_name}(#{signature})
82
- r = ractor_cache.#{method_name}#{compile_lookup}
83
- return r unless r == EMPTY_CACHE
84
-
85
- r = super
86
- ractor_cache.#{method_name}#{compile_lookup} = r unless ractor_cache.frozen?
87
- r
88
- end
89
- RUBY
90
- end
91
-
92
- def deep_freeze_callback(instance)
93
- # nothing to do
94
- end
95
- end
96
-
97
- MAP = {
98
- prebuild: Prebuild,
99
- disable: Disable,
100
- }.freeze
101
- private_constant :MAP
102
-
103
- class << self
104
- def [](kind)
105
- MAP.fetch(kind)
106
- end
107
-
108
- def new(
109
- strategy = nil, # => (:prebuild | :disable)?
110
- to_cache: # => UnboundMethod
111
- ) # => Strategy
112
- self[strategy || :prebuild].new(to_cache)
113
- rescue ArgumentError
114
- return new(:disable, to_cache: to_cache) if strategy == nil
115
-
116
- raise
117
- end
118
- end
119
- end
120
- end
121
- end