hakuban 0.6.5 → 0.8.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.
@@ -0,0 +1,189 @@
1
+ require 'hakuban/refinements'
2
+
3
+ #TODO: bring back Async variants
4
+
5
+ module Hakuban
6
+
7
+ module Stream
8
+
9
+ using ThreadExt
10
+
11
+ class NextItemInterrupt < Exception
12
+ def ==(other)
13
+ other.object_id == self.object_id
14
+ end
15
+ end
16
+
17
+
18
+ # next may be called in CLI context, so, every return branch should process interrupts explicitly
19
+ private def process_item(future_constructor, item_constructor, &block)
20
+ Thread.handle_interrupt(Object => :never) do
21
+ future_pointer = begin
22
+ with_pointer(&future_constructor)
23
+ rescue FFIObject::PointerAlreadyDropped
24
+ Thread.process_interrupts
25
+ return nil
26
+ end
27
+
28
+ result, error = Hakuban::FFI::FFIFuture.await(future_pointer)
29
+ item = if pointer = result.unwrap
30
+ item_constructor.call(pointer)
31
+ end
32
+
33
+ raise error if error
34
+ return item if block.nil?
35
+
36
+ if item.nil?
37
+ Thread.process_interrupts
38
+ nil
39
+ else
40
+ Thread.handle_interrupt(Object => :immediate) {
41
+ block.call(item)
42
+ }
43
+ end
44
+
45
+ ensure
46
+ item.drop if !!block and !!item
47
+ end
48
+ end
49
+
50
+ def each(*args, **kwargs, &block)
51
+ for_each(*args, **kwargs, &block)
52
+ end
53
+
54
+ def for_each(*args, **kwargs, &block)
55
+ if kwargs[:interrupt_on_next]
56
+ for_each_till_next(*args, **kwargs, &block)
57
+ else
58
+ while self.next { |new_item| sync_call(new_item, args, kwargs, block); true }; end
59
+ end
60
+ end
61
+
62
+
63
+ def for_each_till_next(*args, **kwargs, &block)
64
+ for_each_in_thread(*args,**kwargs.merge(kill_previous_on_next: true), &block)
65
+ end
66
+
67
+
68
+ def for_each_concurrent(*args, **kwargs, &block)
69
+ for_each_in_thread(*args, **kwargs, &block)
70
+ end
71
+
72
+
73
+ def for_each_in_thread(*args, **kwargs, &block)
74
+ Thread.handle_interrupt(Object => :never) {
75
+
76
+ # we poll the stream in a separate thread because we sometimes want to interrupt the polling thread from an item-handling thread, and, without the wrapper-thread, we could end up raising exception in a random place of code, if Async is being used. maybe?
77
+ stream_polling_thread = Thread.new {
78
+ begin
79
+ item_threads = ObjectSpace::WeakMap.new
80
+ exception_to_rescue = NextItemInterrupt.new
81
+
82
+ while new_item = self.next
83
+ if kwargs[:kill_previous_on_next]
84
+ item_threads.keys.each { |item_thread|
85
+ item_thread.raise(exception_to_rescue)
86
+ item_thread.join
87
+ }
88
+ end
89
+
90
+ item_threads[Thread.new(new_item, Thread.current) do |item, parent_thread|
91
+ sync_call(item, args, kwargs, block)
92
+ rescue Object => error
93
+ if error != exception_to_rescue
94
+ if kwargs[:propagate_exceptions].nil? or kwargs[:propagate_exceptions]
95
+ parent_thread.raise(error)
96
+ else
97
+ raise
98
+ end
99
+ end
100
+ ensure
101
+ item_threads.delete Thread.current
102
+ end] = true
103
+ end
104
+
105
+ if kwargs[:kill_previous_on_next]
106
+ item_threads.keys.each { |item_thread|
107
+ item_thread.raise(exception_to_rescue)
108
+ item_thread.join
109
+ }
110
+ end
111
+
112
+ Thread.handle_interrupt(Object => :immediate) {
113
+ item_threads.keys.each { |item_thread|
114
+ item_thread.join
115
+ }
116
+ }
117
+
118
+ nil
119
+ rescue Object => error
120
+ exception_to_rescue = error
121
+ item_threads.keys.each { |item_thread| item_thread.raise(error) }
122
+ error
123
+ ensure
124
+ item_threads.keys.each { |item_thread|
125
+ begin
126
+ item_thread.join_with_warning(60)
127
+ rescue Object => error
128
+ # it's probably better to not re-raise here, so this one doesn't obscure the original. but lets at least print it out.
129
+ $stderr.puts "Item thread exception: \n"+error.inspect
130
+ end
131
+ }
132
+ end
133
+ }
134
+
135
+ begin
136
+ Thread.handle_interrupt(Object => :immediate) {
137
+ value = stream_polling_thread.join.value
138
+ raise value if !value.nil?
139
+ }
140
+ rescue Object => error
141
+ stream_polling_thread.raise(error)
142
+ raise
143
+ ensure
144
+ begin
145
+ stream_polling_thread.join_with_warning(60)
146
+ rescue Object => error
147
+ # it's probably better to not re-raise here, so this one doesn't obscure the original. but lets at least print it out.
148
+ $stderr.puts "Item thread exception: \n"+error.inspect
149
+ end
150
+ end
151
+
152
+ }
153
+ end
154
+
155
+
156
+ ## blocks can't be sanely passed to ractors :(
157
+ # def for_each_in_ractor(*args, **kwargs, &block)
158
+ # Thread.handle_interrupt(Object => :never) {
159
+ # begin
160
+ # Thread.handle_interrupt(Object => :immediate) {
161
+ # while new_item = self.next
162
+ # Ractor.new(new_item, args, kwargs, runner ) { |item, args, kwargs, runner|
163
+ # sync_call(item, args, kwargs, runner)
164
+ # }
165
+ # end
166
+ # }
167
+ # ensure
168
+ # #TODO: should we kill still-running sub-tasks here, or await? or maybe kill on exception, and await otherwise?
169
+ # end
170
+ # }
171
+ # end
172
+
173
+
174
+ private def sync_call(item, args, kwargs, block)
175
+ Thread.handle_interrupt(Object => :never) {
176
+ begin
177
+ Thread.handle_interrupt(Object => :immediate) {
178
+ block.call(item, *args, **kwargs)
179
+ }
180
+ ensure
181
+ item.drop
182
+ end
183
+ }
184
+ end
185
+
186
+
187
+ end
188
+
189
+ end
@@ -0,0 +1,39 @@
1
+ require 'hakuban/ffi-object.rb'
2
+
3
+ module Hakuban
4
+
5
+ class Tokio
6
+
7
+ @@pointer = nil
8
+
9
+ def Tokio.init(workers_count=0)
10
+ Hakuban::hakuban_initialize
11
+ @@pointer ||= FFI::hakuban_tokio_init_multi_thread(0)
12
+ end
13
+
14
+ def Tokio.pointer
15
+ Tokio.init if not @@pointer
16
+ @@pointer
17
+ end
18
+
19
+ end
20
+
21
+
22
+ class WebsocketConnector < FFIObject
23
+
24
+ def initialize(exchange, url, &block)
25
+ super()
26
+ Thread.handle_interrupt(Object => :never) {
27
+ pointer = exchange.with_pointer { |exchange_pointer| FFI::hakuban_tokio_websocket_connector_new(Tokio.pointer, exchange_pointer, url) }.unwrap
28
+ initialize_pointer(pointer, :hakuban_tokio_websocket_connector_drop, nil)
29
+ self.do_and_drop_or_return(&block)
30
+ }
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ def self.default_name
38
+ "#{Socket.gethostname}:#{File.basename(caller_locations(0..1)[1].path)}:#{$$}"
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hakuban
4
- VERSION = "0.6.5"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/hakuban.rb CHANGED
@@ -1,10 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "hakuban/version"
4
- require_relative "hakuban/hakuban"
5
- require_relative "hakuban/engine"
6
- #require_relative "hakuban/async"
7
- #require_relative "hakuban/thread"
8
-
9
3
  module Hakuban
4
+
5
+ def self.hakuban_initialize
6
+ require_relative 'hakuban/ffi.rb'
7
+ end
8
+
10
9
  end
10
+
11
+
12
+ require_relative 'hakuban/contract.rb'
13
+ require_relative 'hakuban/descriptor.rb'
14
+ require_relative "hakuban/engine.rb"
15
+ require_relative 'hakuban/exchange.rb'
16
+ require_relative 'hakuban/ffi-object.rb'
17
+ require_relative 'hakuban/logger.rb'
18
+ require_relative 'hakuban/object_state_sink.rb'
19
+ require_relative 'hakuban/object_state_stream.rb'
20
+ require_relative 'hakuban/object_state.rb'
21
+ require_relative 'hakuban/tokio-websocket-connector.rb'
22
+ require_relative "hakuban/version"
23
+
24
+
25
+ #TODO: error classes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hakuban
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.5
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yunta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-26 00:00:00.000000000 Z
11
+ date: 2025-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -16,78 +16,98 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.10'
19
+ version: '3.13'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '3.10'
26
+ version: '3.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: simplecov
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.22'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.22'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: ffi
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - "~>"
32
46
  - !ruby/object:Gem::Version
33
- version: '1.15'
47
+ version: '1.17'
34
48
  type: :runtime
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.15'
54
+ version: '1.17'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: json
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '2.5'
61
+ version: '2.9'
48
62
  type: :runtime
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '2.5'
68
+ version: '2.9'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: slop
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '4.9'
75
+ version: '4.10'
62
76
  type: :runtime
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '4.9'
82
+ version: '4.10'
69
83
  description: Ruby binding for convenient data-object sharing library - Hakuban.
70
84
  email:
71
85
  - maciej.blomberg@mikoton.com
72
86
  executables:
73
- - hakuban-observer
74
- - hakuban-thread-engine
87
+ - hakuban-observe
88
+ - hakuban-engine
75
89
  extensions: []
76
90
  extra_rdoc_files: []
77
91
  files:
78
92
  - MIT-LICENSE
79
93
  - README.md
80
94
  - Rakefile
81
- - bin/hakuban-observer
82
- - bin/hakuban-thread-engine
95
+ - bin/hakuban-engine
96
+ - bin/hakuban-observe
83
97
  - lib/hakuban.rb
84
- - lib/hakuban/async.rb
98
+ - lib/hakuban/contract.rb
99
+ - lib/hakuban/descriptor.rb
85
100
  - lib/hakuban/engine.rb
86
- - lib/hakuban/event-queue.rb
101
+ - lib/hakuban/exchange.rb
102
+ - lib/hakuban/ffi-object.rb
87
103
  - lib/hakuban/ffi.rb
88
- - lib/hakuban/hakuban.rb
89
- - lib/hakuban/manager.rb
90
- - lib/hakuban/thread.rb
104
+ - lib/hakuban/logger.rb
105
+ - lib/hakuban/object_state.rb
106
+ - lib/hakuban/object_state_sink.rb
107
+ - lib/hakuban/object_state_stream.rb
108
+ - lib/hakuban/refinements.rb
109
+ - lib/hakuban/stream.rb
110
+ - lib/hakuban/tokio-websocket-connector.rb
91
111
  - lib/hakuban/version.rb
92
112
  homepage: https://gitlab.com/yunta/hakuban-ruby
93
113
  licenses:
@@ -108,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
128
  - !ruby/object:Gem::Version
109
129
  version: '0'
110
130
  requirements: []
111
- rubygems_version: 3.2.22
131
+ rubygems_version: 3.5.22
112
132
  signing_key:
113
133
  specification_version: 4
114
134
  summary: Ruby binding for Hakuban library
data/bin/hakuban-observer DELETED
@@ -1,64 +0,0 @@
1
- #!/bin/env ruby
2
-
3
- require 'slop'
4
- require 'pp'
5
- require 'hakuban/thread'
6
-
7
-
8
- OPTIONS = Slop.parse { |o|
9
- o.string '-c', '--connect', "Hakuban upstream address (default: ws://127.0.0.1:3001)", default: "ws://127.0.0.1:3001"
10
- o.string '-o', '--object', "Object descriptor"
11
- o.array '-t', '--tag', "Tag descriptor(s)"
12
- o.bool '-d', '--debug', 'Show debug messages'
13
- o.on '-h', '--help' do puts o; exit end
14
- }
15
-
16
- Hakuban.logger_initialize("hakuban=debug") if OPTIONS.debug?
17
- hakuban = Hakuban::LocalNode.new(name: OPTIONS["name"])
18
- connector = Hakuban::WebsocketConnector.new(hakuban, OPTIONS["connect"])
19
-
20
- observe_contract = if OPTIONS["object"]
21
- json = JSON.load(OPTIONS["object"])
22
- tags = OPTIONS["tag"].map { |tag| JSON.load(tag) }
23
- hakuban.object(tags, json).observe
24
- else
25
- tag = OPTIONS["tag"].map { |tag| JSON.load(tag) }
26
- hakuban.tag(tag[0]).observe
27
- end
28
-
29
-
30
- def print_event(descriptor, event, more)
31
- if $subsequent
32
- puts ','
33
- else
34
- $subsequent = true
35
- end
36
- puts '{'
37
- puts '"event": "%s",'%[event]
38
- puts '"descriptor_json": %s,'%[JSON.dump(descriptor.json)]
39
- puts '"descriptor_tags": [%s],'%[descriptor.tags.map { |tag| JSON.dump(tag.json) }.join(",")]
40
- puts more if more
41
- puts '}'
42
- end
43
-
44
-
45
- puts "["
46
-
47
- contract = observe_contract.manage.with_thread { |object|
48
- print_event(object.descriptor, "create", nil)
49
- while object.next_change
50
- print_event(object.descriptor, "change",
51
- if state = object.state
52
- [
53
- '"version": %s,'%[JSON.dump(state.version)],
54
- '"last_sync_ms_ago": %s,'%[JSON.dump(state.synchronized)],
55
- '"data": %s,'%[JSON.dump(state.data)],
56
- ].join("\n")
57
- end
58
- )
59
- end
60
- print_event(object.descriptor, "drop", nil)
61
- }
62
-
63
- $stdin.read(1)
64
- puts "]"
data/lib/hakuban/async.rb DELETED
@@ -1,38 +0,0 @@
1
- require 'hakuban'
2
- require 'hakuban/manager'
3
- require 'async'
4
-
5
- module Hakuban
6
-
7
- class AsyncObjectManager < ObjectManager
8
-
9
- def async_run
10
- Async { yield }
11
- end
12
-
13
- def async_join(task)
14
- task.wait
15
- end
16
-
17
- def async_stop(task)
18
- task.stop
19
- end
20
-
21
- def async_filter_out_stop_exception
22
- yield
23
- rescue Async::Stop
24
- nil
25
- end
26
-
27
- end
28
-
29
-
30
- class ObjectManagerBuilder
31
-
32
- def with_async(&block)
33
- build(AsyncObjectManager, block)
34
- end
35
-
36
- end
37
-
38
- end
@@ -1,75 +0,0 @@
1
- #TODO: rebuild to handle event squashing on rust side
2
-
3
- require 'ostruct'
4
-
5
-
6
- module Hakuban
7
-
8
- class Event < OpenStruct; end
9
-
10
- class ObjectDescriptorCallbackEventQueue
11
-
12
- def initialize(pointer)
13
- @events_pointer = ::FFI::AutoPointer.new(pointer, proc { |ptr| FFI.hakuban_object_descriptor_events_return(ptr) })
14
- end
15
-
16
-
17
- # WARNING: this callback may be run from a separate, non-ruby, thread
18
- def callback_register(&callback)
19
- ffi_callback = proc { |_userdata, ffi_descriptor, ffi_action|
20
- action = Hakuban::action_int_to_symbol(ffi_action)
21
- descriptor = ObjectDescriptor.from_ffi(ffi_descriptor)
22
- callback.call(descriptor, action)
23
- }
24
- @callback_pointer = ::FFI::AutoPointer.new(
25
- FFI::hakuban_object_descriptor_events_callback_register(@events_pointer, ffi_callback, ::FFI::Pointer::NULL),
26
- proc { |ptr| FFI::hakuban_object_descriptor_events_callback_unregister(ptr) }
27
- )
28
- end
29
-
30
-
31
- def callback_unregister
32
- @callback_pointer.free
33
- @callback_pointer = nil
34
- end
35
-
36
-
37
- def drop
38
- @events_pointer.free
39
- @events_pointer = nil
40
- end
41
-
42
- end
43
-
44
-
45
- class ObjectDescriptorEventQueue
46
-
47
- def initialize(contract)
48
- @contract = contract
49
- @queue = Queue.new
50
- @ffi_callback = proc { |descriptor, action|
51
- @queue << Hakuban::Event.new(action: action, descriptor: descriptor)
52
- }
53
- @ffi_events = @contract.new_callback_event_queue
54
- @ffi_events.callback_register(&@ffi_callback)
55
- end
56
-
57
-
58
- #def push(event)
59
- # @queue << event
60
- #end
61
-
62
-
63
- def next_event; next_change; end
64
- def next_change
65
- @queue.pop
66
- end
67
-
68
-
69
- def close
70
- @queue.close
71
- end
72
-
73
- end
74
-
75
- end