hyper-operation 1.0.alpha1.5 → 1.0.alpha1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/hyper-operation.gemspec +6 -5
- data/lib/hyper-operation.rb +2 -1
- data/lib/hyper-operation/api.rb +6 -2
- data/lib/hyper-operation/async_sleep.rb +23 -0
- data/lib/hyper-operation/railway/dispatcher.rb +0 -1
- data/lib/hyper-operation/railway/run.rb +4 -1
- data/lib/hyper-operation/server_op.rb +1 -1
- data/lib/hyper-operation/transport/client_drivers.rb +2 -2
- 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 +9 -1
- data/lib/hyper-operation/transport/hyperstack_controller.rb +5 -1
- data/lib/hyper-operation/transport/policy.rb +9 -4
- data/lib/hyper-operation/version.rb +1 -1
- metadata +64 -42
- 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
|