ractor-cache 0.2.0 → 0.3.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
  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