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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.travis.yml +1 -0
  4. data/hyper-operation.gemspec +6 -4
  5. data/lib/hyper-operation.rb +4 -1
  6. data/lib/hyper-operation/api.rb +6 -2
  7. data/lib/hyper-operation/async_sleep.rb +23 -0
  8. data/lib/hyper-operation/exception.rb +29 -3
  9. data/lib/hyper-operation/promise.rb +32 -2
  10. data/lib/hyper-operation/railway/dispatcher.rb +0 -1
  11. data/lib/hyper-operation/railway/run.rb +57 -48
  12. data/lib/hyper-operation/railway/validations.rb +9 -2
  13. data/lib/hyper-operation/server_op.rb +31 -9
  14. data/lib/hyper-operation/transport/client_drivers.rb +45 -11
  15. data/lib/hyper-operation/transport/connection.rb +58 -136
  16. data/lib/hyper-operation/transport/connection_adapter/active_record.rb +113 -0
  17. data/lib/hyper-operation/transport/connection_adapter/active_record/auto_create.rb +26 -0
  18. data/lib/hyper-operation/transport/connection_adapter/active_record/connection.rb +47 -0
  19. data/lib/hyper-operation/transport/connection_adapter/active_record/queued_message.rb +42 -0
  20. data/lib/hyper-operation/transport/connection_adapter/redis.rb +94 -0
  21. data/lib/hyper-operation/transport/connection_adapter/redis/connection.rb +85 -0
  22. data/lib/hyper-operation/transport/connection_adapter/redis/queued_message.rb +34 -0
  23. data/lib/hyper-operation/transport/connection_adapter/redis/redis_record.rb +158 -0
  24. data/lib/hyper-operation/transport/hyperstack.rb +15 -2
  25. data/lib/hyper-operation/transport/hyperstack_controller.rb +6 -2
  26. data/lib/hyper-operation/transport/policy.rb +16 -26
  27. data/lib/hyper-operation/transport/policy_diagnostics.rb +106 -0
  28. data/lib/hyper-operation/version.rb +1 -1
  29. metadata +79 -38
  30. data/Gemfile.lock +0 -385
  31. 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