hyper-operation 1.0.alpha1.2 → 1.0.alpha1.7
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 +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +1 -0
- data/hyper-operation.gemspec +6 -4
- data/lib/hyper-operation.rb +4 -1
- data/lib/hyper-operation/api.rb +6 -2
- data/lib/hyper-operation/async_sleep.rb +23 -0
- data/lib/hyper-operation/exception.rb +29 -3
- data/lib/hyper-operation/promise.rb +32 -2
- data/lib/hyper-operation/railway/dispatcher.rb +0 -1
- data/lib/hyper-operation/railway/run.rb +57 -48
- data/lib/hyper-operation/railway/validations.rb +9 -2
- data/lib/hyper-operation/server_op.rb +31 -9
- data/lib/hyper-operation/transport/client_drivers.rb +45 -11
- data/lib/hyper-operation/transport/connection.rb +58 -136
- data/lib/hyper-operation/transport/connection_adapter/active_record.rb +113 -0
- data/lib/hyper-operation/transport/connection_adapter/active_record/auto_create.rb +26 -0
- data/lib/hyper-operation/transport/connection_adapter/active_record/connection.rb +47 -0
- data/lib/hyper-operation/transport/connection_adapter/active_record/queued_message.rb +42 -0
- data/lib/hyper-operation/transport/connection_adapter/redis.rb +94 -0
- data/lib/hyper-operation/transport/connection_adapter/redis/connection.rb +85 -0
- data/lib/hyper-operation/transport/connection_adapter/redis/queued_message.rb +34 -0
- data/lib/hyper-operation/transport/connection_adapter/redis/redis_record.rb +158 -0
- data/lib/hyper-operation/transport/hyperstack.rb +15 -2
- data/lib/hyper-operation/transport/hyperstack_controller.rb +6 -2
- data/lib/hyper-operation/transport/policy.rb +16 -26
- data/lib/hyper-operation/transport/policy_diagnostics.rb +106 -0
- data/lib/hyper-operation/version.rb +1 -1
- metadata +79 -38
- data/Gemfile.lock +0 -385
- data/lib/hyper-operation/delay_and_interval.rb +0 -9
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hyperstack
|
4
|
+
module ConnectionAdapter
|
5
|
+
module ActiveRecord
|
6
|
+
module AutoCreate
|
7
|
+
def table_exists?
|
8
|
+
# works with both rails 4 and 5 without deprecation warnings
|
9
|
+
if connection.respond_to?(:data_sources)
|
10
|
+
connection.data_sources.include?(table_name)
|
11
|
+
else
|
12
|
+
connection.tables.include?(table_name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def needs_init?
|
17
|
+
Hyperstack.transport != :none && Hyperstack.on_server? && !table_exists?
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_table(*args, &block)
|
21
|
+
connection.create_table(table_name, *args, &block) if needs_init?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'auto_create'
|
4
|
+
|
5
|
+
module Hyperstack
|
6
|
+
module ConnectionAdapter
|
7
|
+
module ActiveRecord
|
8
|
+
class Connection < ::ActiveRecord::Base
|
9
|
+
extend AutoCreate
|
10
|
+
|
11
|
+
self.table_name = 'hyperstack_connections'
|
12
|
+
|
13
|
+
do_not_synchronize
|
14
|
+
|
15
|
+
has_many :messages,
|
16
|
+
foreign_key: 'connection_id',
|
17
|
+
class_name: 'Hyperstack::ConnectionAdapter::ActiveRecord::QueuedMessage',
|
18
|
+
dependent: :destroy
|
19
|
+
|
20
|
+
scope :expired,
|
21
|
+
-> { where('expires_at IS NOT NULL AND expires_at < ?', Time.current) }
|
22
|
+
scope :pending_for,
|
23
|
+
->(channel) { where(channel: channel).where('session IS NOT NULL') }
|
24
|
+
scope :inactive,
|
25
|
+
-> { where('session IS NULL AND refresh_at < ?', Time.current) }
|
26
|
+
|
27
|
+
before_create do
|
28
|
+
if session
|
29
|
+
self.expires_at = Time.current + transport.expire_new_connection_in
|
30
|
+
elsif transport.refresh_channels_every != :never
|
31
|
+
self.refresh_at = Time.current + transport.refresh_channels_every
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def needs_refresh?
|
37
|
+
exists?(['refresh_at IS NOT NULL AND refresh_at < ?', Time.current])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def transport
|
42
|
+
Hyperstack::Connection.transport
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'auto_create'
|
4
|
+
|
5
|
+
module Hyperstack
|
6
|
+
module ConnectionAdapter
|
7
|
+
module ActiveRecord
|
8
|
+
class QueuedMessage < ::ActiveRecord::Base
|
9
|
+
extend AutoCreate
|
10
|
+
|
11
|
+
self.table_name = 'hyperstack_queued_messages'
|
12
|
+
|
13
|
+
do_not_synchronize
|
14
|
+
|
15
|
+
serialize :data
|
16
|
+
|
17
|
+
belongs_to :hyperstack_connection,
|
18
|
+
class_name: 'Hyperstack::ConnectionAdapter::ActiveRecord::Connection',
|
19
|
+
foreign_key: 'connection_id',
|
20
|
+
optional: true
|
21
|
+
|
22
|
+
scope :for_session,
|
23
|
+
->(session) { joins(:hyperstack_connection).where('session = ?', session) }
|
24
|
+
|
25
|
+
# For simplicity we use QueuedMessage with connection_id 0
|
26
|
+
# to store the current path which is used by consoles to
|
27
|
+
# communicate back to the server. The belongs_to connection
|
28
|
+
# therefore must be optional.
|
29
|
+
|
30
|
+
default_scope { where('connection_id IS NULL OR connection_id != 0') }
|
31
|
+
|
32
|
+
def self.root_path=(path)
|
33
|
+
unscoped.find_or_create_by(connection_id: 0).update(data: path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.root_path
|
37
|
+
unscoped.find_or_create_by(connection_id: 0).data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'redis/connection'
|
4
|
+
require_relative 'redis/queued_message'
|
5
|
+
|
6
|
+
module Hyperstack
|
7
|
+
module ConnectionAdapter
|
8
|
+
module Redis
|
9
|
+
class << self
|
10
|
+
def transport
|
11
|
+
Hyperstack::Connection.transport
|
12
|
+
end
|
13
|
+
|
14
|
+
def active
|
15
|
+
if Hyperstack.on_server?
|
16
|
+
Connection.expired.each(&:destroy)
|
17
|
+
refresh_connections if Connection.needs_refresh?
|
18
|
+
end
|
19
|
+
|
20
|
+
Connection.all.map(&:channel).uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
def open(channel, session = nil, root_path = nil)
|
24
|
+
self.root_path = root_path
|
25
|
+
|
26
|
+
Connection.find_or_create_by(channel: channel, session: session)
|
27
|
+
end
|
28
|
+
|
29
|
+
def send_to_channel(channel, data)
|
30
|
+
Connection.pending_for(channel).each do |connection|
|
31
|
+
QueuedMessage.create(connection_id: connection.id, data: data)
|
32
|
+
end
|
33
|
+
|
34
|
+
transport.send_data(channel, data) if Connection.exists?(channel: channel, session: nil)
|
35
|
+
end
|
36
|
+
|
37
|
+
def read(session, root_path)
|
38
|
+
self.root_path = root_path
|
39
|
+
|
40
|
+
Connection.where(session: session).each do |connection|
|
41
|
+
connection.update(expires_at: Time.current + transport.expire_polled_connection_in)
|
42
|
+
end
|
43
|
+
|
44
|
+
messages = QueuedMessage.for_session(session)
|
45
|
+
data = messages.map(&:data)
|
46
|
+
messages.each(&:destroy)
|
47
|
+
data
|
48
|
+
end
|
49
|
+
|
50
|
+
def connect_to_transport(channel, session, root_path)
|
51
|
+
self.root_path = root_path
|
52
|
+
|
53
|
+
if (connection = Connection.find_by(channel: channel, session: session))
|
54
|
+
messages = connection.messages.map(&:data)
|
55
|
+
connection.destroy
|
56
|
+
else
|
57
|
+
messages = []
|
58
|
+
end
|
59
|
+
|
60
|
+
open(channel)
|
61
|
+
|
62
|
+
messages
|
63
|
+
end
|
64
|
+
|
65
|
+
def disconnect(channel)
|
66
|
+
Connection.find_by(channel: channel, session: nil).destroy
|
67
|
+
end
|
68
|
+
|
69
|
+
def root_path=(path)
|
70
|
+
QueuedMessage.root_path = path if path
|
71
|
+
end
|
72
|
+
|
73
|
+
def root_path
|
74
|
+
QueuedMessage.root_path
|
75
|
+
rescue
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def refresh_connections
|
80
|
+
refresh_started_at = Time.current
|
81
|
+
channels = transport.refresh_channels
|
82
|
+
next_refresh = refresh_started_at + transport.refresh_channels_every
|
83
|
+
|
84
|
+
channels.each do |channel|
|
85
|
+
connection = Connection.find_by(channel: channel, session: nil)
|
86
|
+
connection.update(refresh_at: next_refresh) if connection
|
87
|
+
end
|
88
|
+
|
89
|
+
Connection.inactive.each(&:destroy)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'redis_record'
|
4
|
+
|
5
|
+
module Hyperstack
|
6
|
+
module ConnectionAdapter
|
7
|
+
module Redis
|
8
|
+
class Connection < RedisRecord::Base
|
9
|
+
self.table_name = 'hyperstack:connections'
|
10
|
+
self.column_names = %w[id channel session created_at expires_at refresh_at].freeze
|
11
|
+
|
12
|
+
attr_accessor(*column_names.map(&:to_sym))
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def transport
|
16
|
+
Hyperstack::Connection.transport
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(opts = {})
|
20
|
+
opts.tap do |hash|
|
21
|
+
if opts[:session]
|
22
|
+
hash[:expires_at] = (Time.current + transport.expire_new_connection_in)
|
23
|
+
elsif transport.refresh_channels_every != :never
|
24
|
+
hash[:refresh_at] = (Time.current + transport.refresh_channels_every)
|
25
|
+
end
|
26
|
+
|
27
|
+
hash[:created_at] = Time.current
|
28
|
+
end.to_a.flatten
|
29
|
+
|
30
|
+
super(opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
def inactive
|
34
|
+
scope { |id| id if inactive?(id) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def inactive?(id)
|
38
|
+
get_dejsonized_attribute(id, :session).blank? &&
|
39
|
+
get_dejsonized_attribute(id, :refresh_at).present? &&
|
40
|
+
Time.zone.parse(get_dejsonized_attribute(id, :refresh_at)) < Time.current
|
41
|
+
end
|
42
|
+
|
43
|
+
def expired
|
44
|
+
scope { |id| id if expired?(id) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def expired?(id)
|
48
|
+
get_dejsonized_attribute(id, :expires_at).present? &&
|
49
|
+
Time.zone.parse(get_dejsonized_attribute(id, :expires_at)) < Time.current
|
50
|
+
end
|
51
|
+
|
52
|
+
def pending_for(channel)
|
53
|
+
scope { |id| id if pending_for?(id, channel) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def pending_for?(id, channel)
|
57
|
+
get_dejsonized_attribute(id, :session).present? &&
|
58
|
+
get_dejsonized_attribute(id, :channel) == channel
|
59
|
+
end
|
60
|
+
|
61
|
+
def needs_refresh?
|
62
|
+
scope { |id| id if needs_refresh(id) }.any?
|
63
|
+
end
|
64
|
+
|
65
|
+
def needs_refresh(id)
|
66
|
+
get_dejsonized_attribute(id, :refresh_at).present? &&
|
67
|
+
Time.zone.parse(get_dejsonized_attribute(id, :refresh_at)) < Time.current
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def messages
|
72
|
+
QueuedMessage.where(connection_id: id)
|
73
|
+
end
|
74
|
+
|
75
|
+
%i[created_at expires_at refresh_at].each do |attr|
|
76
|
+
define_method(attr) do
|
77
|
+
value = instance_variable_get(:"@#{attr}")
|
78
|
+
|
79
|
+
value.is_a?(Time) ? value : Time.zone.parse(value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'redis_record'
|
4
|
+
|
5
|
+
module Hyperstack
|
6
|
+
module ConnectionAdapter
|
7
|
+
module Redis
|
8
|
+
class QueuedMessage < RedisRecord::Base
|
9
|
+
self.table_name = 'hyperstack:queued_messages'
|
10
|
+
self.column_names = %w[id data connection_id].freeze
|
11
|
+
|
12
|
+
attr_accessor(*column_names.map(&:to_sym))
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def for_session(session)
|
16
|
+
Connection.where(session: session).map(&:messages).flatten
|
17
|
+
end
|
18
|
+
|
19
|
+
def root_path=(path)
|
20
|
+
find_or_create_by(connection_id: 0).update(data: path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def root_path
|
24
|
+
find_or_create_by(connection_id: 0).data
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def connection
|
29
|
+
Connection.find(connection_id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hyperstack
|
4
|
+
module ConnectionAdapter
|
5
|
+
module Redis
|
6
|
+
module RedisRecord
|
7
|
+
class Base
|
8
|
+
class << self
|
9
|
+
attr_accessor :table_name, :column_names
|
10
|
+
|
11
|
+
def client
|
12
|
+
@client ||= ::Redis.new(url: Hyperstack.connection[:redis_url])
|
13
|
+
end
|
14
|
+
|
15
|
+
def scope(&block)
|
16
|
+
ids = client.smembers(table_name)
|
17
|
+
|
18
|
+
ids = ids.map(&block) if block
|
19
|
+
|
20
|
+
ids.compact.map { |id| instantiate(id) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def all
|
24
|
+
scope
|
25
|
+
end
|
26
|
+
|
27
|
+
def first
|
28
|
+
id = client.smembers(table_name).first
|
29
|
+
|
30
|
+
instantiate(id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def last
|
34
|
+
id = client.smembers(table_name).last
|
35
|
+
|
36
|
+
instantiate(id)
|
37
|
+
end
|
38
|
+
|
39
|
+
def find(id)
|
40
|
+
return unless client.smembers(table_name).include?(id)
|
41
|
+
|
42
|
+
instantiate(id)
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_by(opts)
|
46
|
+
found = nil
|
47
|
+
|
48
|
+
client.smembers(table_name).each do |id|
|
49
|
+
unless opts.map { |k, v| get_dejsonized_attribute(id, k) == v }.include?(false)
|
50
|
+
found = instantiate(id)
|
51
|
+
break
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
found
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_or_create_by(opts = {})
|
59
|
+
if (existing = find_by(opts))
|
60
|
+
existing
|
61
|
+
else
|
62
|
+
create(opts)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def where(opts = {})
|
67
|
+
scope do |id|
|
68
|
+
unless opts.map { |k, v| get_dejsonized_attribute(id, k) == v }.include?(false)
|
69
|
+
id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def exists?(opts = {})
|
75
|
+
!!client.smembers(table_name).detect do |id|
|
76
|
+
!opts.map { |k, v| get_dejsonized_attribute(id, k) == v }.include?(false)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def create(opts = {})
|
81
|
+
record = new({ id: SecureRandom.uuid }.merge(opts))
|
82
|
+
|
83
|
+
record.save
|
84
|
+
|
85
|
+
record
|
86
|
+
end
|
87
|
+
|
88
|
+
def destroy_all
|
89
|
+
all.each(&:destroy)
|
90
|
+
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
def jsonize_attributes(attrs)
|
95
|
+
attrs.map do |attr, value|
|
96
|
+
[attr, value.to_json]
|
97
|
+
end.to_h
|
98
|
+
end
|
99
|
+
|
100
|
+
def dejsonize_attributes(attrs)
|
101
|
+
attrs.map do |attr, value|
|
102
|
+
[attr, value && JSON.parse(value)]
|
103
|
+
end.to_h
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def instantiate(id)
|
109
|
+
new(dejsonize_attributes(client.hgetall("#{table_name}:#{id}")))
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_dejsonized_attribute(id, attr)
|
113
|
+
value = client.hget("#{table_name}:#{id}", attr)
|
114
|
+
JSON.parse(value) if value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def initialize(opts = {})
|
119
|
+
opts.each { |k, v| send(:"#{k}=", v) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def save
|
123
|
+
self.class.client.hmset("#{table_name}:#{id}", *self.class.jsonize_attributes(attributes))
|
124
|
+
|
125
|
+
unless self.class.client.smembers(table_name).include?(id)
|
126
|
+
self.class.client.sadd(table_name, id)
|
127
|
+
end
|
128
|
+
|
129
|
+
true
|
130
|
+
end
|
131
|
+
|
132
|
+
def update(opts = {})
|
133
|
+
opts.each { |k, v| send(:"#{k}=", v) }
|
134
|
+
save
|
135
|
+
end
|
136
|
+
|
137
|
+
def destroy
|
138
|
+
self.class.client.srem(table_name, id)
|
139
|
+
|
140
|
+
self.class.client.hdel("#{table_name}:#{id}", attributes.keys)
|
141
|
+
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
def attributes
|
146
|
+
self.class.column_names.map do |column_name|
|
147
|
+
[column_name, instance_variable_get("@#{column_name}")]
|
148
|
+
end.to_h
|
149
|
+
end
|
150
|
+
|
151
|
+
def table_name
|
152
|
+
self.class.table_name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|