ractor-tmvar 0.3.0 → 0.4.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: bd148e7f28224c4301b6ec77b004a483722f53b4fdd35d3e69886d06358db39d
4
- data.tar.gz: c6b21f4ce16240b9d5912dc6528feeb5cddcb0bac6d8dc81ea8cd0cbd0bd1ee5
3
+ metadata.gz: 6f3245eebf48796294d345ac5d97677c3f536347f87a204555223781cec1a87d
4
+ data.tar.gz: 3bf7ca844e8556db49d981b2d50c6e00867927f70348d59242220b66d0e20a59
5
5
  SHA512:
6
- metadata.gz: 7272a5419621fae9ef87a3cbfd895b9a21d6429b009bb6a3af32a56c62d07a6583e7c86e4f8438e1859055755644f8e2e35ccd8f244f35a742a627b634d579d8
7
- data.tar.gz: '08abed6a02c3e4832ef99ca5139cf625a85e66559482758371f380fa590669dc7983e0ca14896a01514a1a474254f207a8ecb7987a65029f7fbbecb7e7f0363d'
6
+ metadata.gz: b30e17fdb2e91c2aa60e83edf47098926a017a63a192a409545116ef7ec605ab2203361f00862de5e8f2e443058dc3c7111b69a77bf4373d9f718e74faff5c1c
7
+ data.tar.gz: 567905c4e3ae0b351a0ecaaa69b97a5de09d77e7047da0e07cee7e45818251097e2e12bf32e3ebf173bfff48ccf3dada4ba40c29ad153a87da2176028f098058
@@ -0,0 +1,54 @@
1
+ name: Build and attest gem
2
+
3
+ on:
4
+ workflow_call:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ id-token: write
15
+ attestations: write
16
+ steps:
17
+ - name: Check out tagged source
18
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
19
+ with:
20
+ ref: ${{ github.sha }}
21
+ persist-credentials: false
22
+
23
+ - name: Set up Ruby
24
+ uses: ruby/setup-ruby@e81a8fa391b11a595c1b7ed84177cc0fe02faf83 # v1
25
+ with:
26
+ ruby-version: '4.0'
27
+ bundler-cache: true
28
+
29
+ - name: Check release tag matches gem version
30
+ run: |
31
+ ruby -r ./lib/ractor/tmvar/version -e '
32
+ expected = "refs/tags/v#{Ractor::TMVar::VERSION}"
33
+ abort "Expected #{expected}, got #{ENV.fetch("GITHUB_REF")}" unless ENV.fetch("GITHUB_REF") == expected
34
+ '
35
+
36
+ - name: Run tests and lint
37
+ run: bundle exec rake
38
+
39
+ - name: Build gem
40
+ run: |
41
+ mkdir -p pkg
42
+ gem build ractor-tmvar.gemspec --output "pkg/ractor-tmvar-${GITHUB_REF_NAME#v}.gem"
43
+
44
+ - name: Attest gem provenance
45
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
46
+ with:
47
+ subject-path: pkg/*.gem
48
+
49
+ - name: Save gem
50
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
51
+ with:
52
+ name: ractor-tmvar-${{ github.ref_name }}
53
+ path: pkg/*.gem
54
+ if-no-files-found: error
@@ -0,0 +1,17 @@
1
+ name: Build release gem
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ permissions:
14
+ contents: read
15
+ id-token: write
16
+ attestations: write
17
+ uses: ./.github/workflows/build-gem.yml
data/.gitignore CHANGED
@@ -7,3 +7,6 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  /vendor/
10
+ .pi
11
+ .env
12
+ .env.local
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ractor-tmvar (0.3.0)
4
+ ractor-tmvar (0.4.0)
5
5
  ractor-sharing (~> 0.3.0)
6
6
 
7
7
  GEM
@@ -69,7 +69,7 @@ CHECKSUMS
69
69
  prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
70
70
  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
71
71
  ractor-sharing (0.3.0) sha256=9adb2f899e4e38f477a2d3c02368e8ff40be96b119172312253338ee1e22f63f
72
- ractor-tmvar (0.3.0)
72
+ ractor-tmvar (0.4.0)
73
73
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
74
74
  rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
75
75
  regexp_parser (2.13.1) sha256=5aedb6b7c35688f51e86eef17a15374dfd287c83f4ca644b2c5f3ec50a33b44b
data/README.md CHANGED
@@ -47,11 +47,74 @@ tv1.read #=> 100
47
47
  tv2.read #=> 200
48
48
  ```
49
49
 
50
+ `Ractor::TMVar.new` creates an empty variable, while `Ractor::TMVar.new(value)` or
51
+ `Ractor::TMVar.new_filled(value)` creates a variable already containing `value`.
52
+ `nil` can be stored as a normal value:
53
+
54
+ ```ruby
55
+ tv = Ractor::TMVar.new_filled(nil)
56
+ tv.empty? #=> false
57
+ tv.read #=> nil
58
+ ```
59
+
60
+ ### Transactional operations
61
+
62
+ `take`, `put`, `swap`, `try_take`, and `try_put` must be called inside `Ractor.atomically`. `read` and
63
+ `empty?` can be called outside of a transaction.
64
+
65
+ ```ruby
66
+ tv = Ractor::TMVar.new
67
+ Ractor.atomically { tv.put(42) }
68
+ Ractor.atomically { tv.take } #=> 42
69
+ ```
70
+
71
+ ### Non-blocking try operations
72
+
73
+ `try_take` and `try_read` never retry. Without a block, they return the stored
74
+ value, or `nil` when empty. With a block, when the variable is
75
+ empty they return `nil` without calling the block. When it is full they yield the
76
+ stored value and return the block's result, so a stored `nil` can be told apart
77
+ from an empty variable. `try_take` empties the variable before yielding;
78
+ `try_read` leaves it full.
79
+
80
+ ```ruby
81
+ tv = Ractor::TMVar.new(5)
82
+
83
+ tv.try_read { |v| v * 2 } #=> 10
84
+ tv.read #=> 5
85
+
86
+ Ractor.atomically { tv.try_take { |v| v * 2 } } #=> 10
87
+ tv.empty? #=> true
88
+
89
+ empty = Ractor::TMVar.new
90
+ Ractor.atomically { empty.try_take { |v| [v] } } #=> nil (block not called)
91
+
92
+ filled_with_nil = Ractor::TMVar.new_filled(nil)
93
+ filled_with_nil.try_read { |v| [v] } #=> [nil]
94
+ ```
95
+
96
+ `try_put` returns `true` when it stored the value and `false` when the variable
97
+ was already full.
98
+
99
+ ```ruby
100
+ tv = Ractor::TMVar.new
101
+ Ractor.atomically { tv.try_put(1) } #=> true
102
+ Ractor.atomically { tv.try_put(2) } #=> false
103
+ ```
104
+
50
105
  ## Development
51
106
 
52
107
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
53
108
 
54
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
109
+ To install this gem onto your local machine, run `bundle exec rake install`.
110
+
111
+ ## Verifying gem provenance
112
+
113
+ Verify a downloaded gem's build provenance attestation with the GitHub CLI:
114
+
115
+ ```sh
116
+ gh attestation verify ractor-tmvar-0.3.0.gem -R yoshitsugu/ractor-tmvar
117
+ ```
55
118
 
56
119
  ## Contributing
57
120
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  class Ractor
4
4
  class TMVar
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
data/lib/ractor/tmvar.rb CHANGED
@@ -1,134 +1,229 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ractor/tvar"
4
+ require_relative "tmvar/version"
4
5
 
5
6
  class Ractor
6
- # TMVar for Ractor inspired by Haskell's TMVar based on Ractor::TVar.
7
+ # Transactional MVar-like variable for Ractor, inspired by Haskell's
8
+ # +Control.Concurrent.STM.TMVar+ and built on Ractor::TVar.
9
+ #
10
+ # A TMVar is either empty or full. When full it holds a single value.
11
+ # Reading and writing operations are transactional through {Ractor.atomically}.
12
+ #
13
+ # The implementation stores +nil+ when empty and a single-element array +[value]+
14
+ # when full, so any Ruby object (including +nil+) can be stored without colliding
15
+ # with the empty state.
7
16
  class TMVar
8
17
  #
9
- # represents "empty" value for TMVar
18
+ # Create a new TMVar.
10
19
  #
11
- EMPTY = :RACTOR_TMVAR_EMPTY
20
+ # @overload initialize
21
+ # Creates an empty TMVar (Haskell's +newEmptyTMVar+).
22
+ # @overload initialize(value)
23
+ # Creates a TMVar filled with +value+ (Haskell's +newTMVar+).
24
+ # @param [Object] value value to store. With ractor-sharing 0.3, the value is
25
+ # automatically made Ractor-shareable and the passed object is frozen.
26
+ # This behavior is provided by the upstream Ractor::TVar, not a TMVar guarantee.
27
+ #
28
+ # @raise [ArgumentError] if more than one argument is given.
29
+ #
30
+ def initialize(*args)
31
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)" if args.size > 1
32
+
33
+ @tvar = Ractor::TVar.new(args.empty? ? nil : [args[0]])
34
+ end
12
35
 
13
36
  #
14
- # initialize TMVar
37
+ # Create a new TMVar explicitly filled with +value+.
15
38
  #
16
- # @param [Object] value value to set. With ractor-sharing 0.3, the value is
39
+ # This is equivalent to +TMVar.new(value)+ but makes the intent clear when the
40
+ # stored value might be +nil+.
41
+ #
42
+ # @param [Object] value value to store. With ractor-sharing 0.3, the value is
17
43
  # automatically made Ractor-shareable and the passed object is frozen.
18
44
  # This behavior is provided by the upstream Ractor::TVar, not a TMVar guarantee.
19
45
  #
20
46
  # @return [TMVar]
21
47
  #
22
- def initialize(value = nil)
23
- @tvar = Ractor::TVar.new(value)
48
+ def self.new_filled(value)
49
+ new(value)
24
50
  end
25
51
 
26
52
  #
27
- # get the value and leave the value to "empty"
28
- # If the value is already "empty", it will retry the transaction.
29
- # @note You need to wrap it by Ractor.atomically even if you only call +take+
30
- # because +TVar#value=+ needs atomically.
53
+ # Take the value, leaving the TMVar empty.
54
+ #
55
+ # If the TMVar is empty, the transaction is retried.
56
+ #
57
+ # @note Must be called inside {Ractor.atomically}.
31
58
  #
32
- # @return [Object] value of internal TVar.
59
+ # @return [Object] the stored value.
33
60
  #
34
61
  def take
35
- v = @tvar.value
36
- raise Ractor::RetryTransaction if v == EMPTY
62
+ cell = @tvar.value
63
+ raise Ractor::RetryTransaction if cell.nil?
37
64
 
38
- @tvar.value = EMPTY
39
- v
65
+ @tvar.value = nil
66
+ cell[0]
40
67
  end
41
68
 
42
69
  #
43
- # try to get the value.
44
- # If the value is "empty", it returns nil.
70
+ # Try to take the value without retrying.
45
71
  #
46
- # @return [Object] value of internal TVar, only if exists.
72
+ # If the TMVar is empty, returns +nil+ without calling the block.
73
+ # If the TMVar is full, empties it, yields the value and returns the block's result.
74
+ # Without a block, returns the stored value unchanged.
75
+ # Because the block is called only when full, a stored +nil+ can be told apart
76
+ # from an empty TMVar.
47
77
  #
48
- def try_take
49
- v = @tvar.value
50
- return nil if v == EMPTY
78
+ # @note Must be called inside {Ractor.atomically}.
79
+ #
80
+ # @overload try_take
81
+ # @return [Object, nil] the stored value, or +nil+ if empty.
82
+ # @overload try_take { |value| ... }
83
+ # @yieldparam value [Object] the stored value, including +nil+.
84
+ # @yieldreturn [Object] the value to return from +try_take+.
85
+ # @return [Object, nil] the block's result, or +nil+ if empty.
86
+ #
87
+ # @example Take a value without a block
88
+ # tv = Ractor::TMVar.new(5)
89
+ # Ractor.atomically { tv.try_take } #=> 5
90
+ # tv.empty? #=> true
91
+ #
92
+ # @example Distinguish a stored nil from an empty TMVar
93
+ # tv = Ractor::TMVar.new(nil)
94
+ # Ractor.atomically { tv.try_take { |value| [value] } } #=> [nil]
95
+ # Ractor.atomically { tv.try_take { |value| [value] } } #=> nil
96
+ #
97
+ def try_take(&block)
98
+ cell = @tvar.value
99
+ return nil if cell.nil?
51
100
 
52
- @tvar.value = EMPTY
53
- v
101
+ @tvar.value = nil
102
+ (block || method(:identity)).call(cell[0])
54
103
  end
55
104
 
56
105
  #
57
- # get the value like +take+.
58
- # The difference between +take+ and +read+ is +take+ leaves the value blank,
59
- # but +read+ not change the value to blank.
106
+ # Read the value, leaving the TMVar full.
60
107
  #
61
- # @return [Object] value of internal TVar.
108
+ # If the TMVar is empty, the transaction is retried.
109
+ # It can also be called outside {Ractor.atomically}.
110
+ #
111
+ # @return [Object] the stored value.
62
112
  #
63
113
  def read
64
- v = @tvar.value
65
- raise Ractor::RetryTransaction if v == EMPTY
114
+ cell = @tvar.value
115
+ raise Ractor::RetryTransaction if cell.nil?
66
116
 
67
- v
117
+ cell[0]
68
118
  end
69
119
 
70
120
  #
71
- # read the value like +read+ but it does not retry.
121
+ # Try to read the value without retrying.
122
+ #
123
+ # If the TMVar is empty, returns +nil+ without calling the block.
124
+ # If the TMVar is full, yields the value and returns the block's result.
125
+ # Without a block, returns the stored value unchanged.
126
+ # The TMVar remains full.
127
+ #
128
+ # @note Can be called outside {Ractor.atomically}.
72
129
  #
73
- # @return [Object] value of internal TVar.
130
+ # @overload try_read
131
+ # @return [Object, nil] the stored value, or +nil+ if empty.
132
+ # @overload try_read { |value| ... }
133
+ # @yieldparam value [Object] the stored value, including +nil+.
134
+ # @yieldreturn [Object] the value to return from +try_read+.
135
+ # @return [Object, nil] the block's result, or +nil+ if empty.
74
136
  #
75
- def try_read
76
- v = @tvar.value
77
- v == EMPTY ? nil : v
137
+ # @example Read or transform a value without removing it
138
+ # tv = Ractor::TMVar.new(5)
139
+ # tv.try_read #=> 5
140
+ # tv.try_read { |value| value * 2 } #=> 10
141
+ # tv.read #=> 5
142
+ #
143
+ # @example Distinguish a stored nil from an empty TMVar
144
+ # Ractor::TMVar.new(nil).try_read { |value| [value] } #=> [nil]
145
+ # Ractor::TMVar.new.try_read { |value| [value] } #=> nil
146
+ #
147
+ def try_read(&block)
148
+ cell = @tvar.value
149
+ return nil if cell.nil?
150
+
151
+ (block || method(:identity)).call(cell[0])
78
152
  end
79
153
 
80
154
  #
81
- # write the given value.
82
- # If the current value is not "empty", it retries the transaction.
155
+ # Put a value into the TMVar.
156
+ #
157
+ # If the TMVar is already full, the transaction is retried.
83
158
  #
84
- # @param [Object] new_value value to set. With ractor-sharing 0.3, the value is
159
+ # @note Must be called inside {Ractor.atomically}.
160
+ #
161
+ # @param [Object] new_value value to store. With ractor-sharing 0.3, the value is
85
162
  # automatically made Ractor-shareable and the passed object is frozen.
86
163
  # This behavior is provided by the upstream Ractor::TVar, not a TMVar guarantee.
87
164
  #
165
+ # @return [Object] +new_value+.
166
+ #
88
167
  def put(new_value)
89
- raise Ractor::RetryTransaction if @tvar.value != EMPTY
168
+ raise Ractor::RetryTransaction unless @tvar.value.nil?
90
169
 
91
- @tvar.value = new_value
170
+ @tvar.value = [new_value]
171
+ new_value
92
172
  end
93
173
 
94
174
  #
95
- # try to put value to TVar's value
96
- # If the value is not "empty", it will not retry and only return false.
97
- # If it succeed to put, it returns true.
175
+ # Try to put a value into the TMVar without retrying.
176
+ #
177
+ # @note Must be called inside {Ractor.atomically}.
98
178
  #
99
- # @param [Object] new_value value to set. With ractor-sharing 0.3, the value is
179
+ # @param [Object] new_value value to store. With ractor-sharing 0.3, the value is
100
180
  # automatically made Ractor-shareable and the passed object is frozen.
101
181
  # This behavior is provided by the upstream Ractor::TVar, not a TMVar guarantee.
102
182
  #
183
+ # @return [Boolean] +true+ if the value was stored, +false+ if the TMVar was full.
184
+ #
103
185
  def try_put(new_value)
104
- return false if @tvar.value != EMPTY
186
+ return false unless @tvar.value.nil?
105
187
 
106
- @tvar.value = new_value
188
+ @tvar.value = [new_value]
107
189
  true
108
190
  end
109
191
 
110
192
  #
111
- # return the value is "empty" or not.
193
+ # Return whether the TMVar is currently empty.
194
+ # It can also be called outside {Ractor.atomically}.
112
195
  #
113
- # @param [Boolean]
196
+ # @return [Boolean]
114
197
  #
115
198
  def empty?
116
- @tvar.value == EMPTY
199
+ @tvar.value.nil?
117
200
  end
118
201
 
119
202
  #
120
- # get the the value like +get+, and replace the value to the given value if the current value is not "empty"
203
+ # Replace the stored value and return the previous value.
204
+ #
205
+ # If the TMVar is empty, the transaction is retried.
121
206
  #
122
- # @param [Object] new value to set. With ractor-sharing 0.3, the value is
207
+ # @note Must be called inside {Ractor.atomically}.
208
+ #
209
+ # @param [Object] new_value value to store. With ractor-sharing 0.3, the value is
123
210
  # automatically made Ractor-shareable and the passed object is frozen.
124
211
  # This behavior is provided by the upstream Ractor::TVar, not a TMVar guarantee.
125
212
  #
126
- def swap(new)
127
- v = @tvar.value
128
- raise Ractor::RetryTransaction if v == EMPTY
213
+ # @return [Object] the previous value.
214
+ #
215
+ def swap(new_value)
216
+ cell = @tvar.value
217
+ raise Ractor::RetryTransaction if cell.nil?
218
+
219
+ @tvar.value = [new_value]
220
+ cell[0]
221
+ end
222
+
223
+ private
129
224
 
130
- @tvar.value = new
131
- v
225
+ def identity(value)
226
+ value
132
227
  end
133
228
  end
134
229
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ractor-tmvar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yoshitsugu
@@ -30,7 +30,9 @@ executables: []
30
30
  extensions: []
31
31
  extra_rdoc_files: []
32
32
  files:
33
+ - ".github/workflows/build-gem.yml"
33
34
  - ".github/workflows/main.yml"
35
+ - ".github/workflows/release.yml"
34
36
  - ".gitignore"
35
37
  - ".rubocop.yml"
36
38
  - Gemfile
@@ -65,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
67
  - !ruby/object:Gem::Version
66
68
  version: '0'
67
69
  requirements: []
68
- rubygems_version: 4.0.3
70
+ rubygems_version: 4.0.20
69
71
  specification_version: 4
70
72
  summary: Ractor::TMVar
71
73
  test_files: []