edgycircle_kommando 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2962518e6deb43c8cd51657f311ebbd96caf84d0e04cfb7505c7c427fddec0b
4
- data.tar.gz: 8cdfe6722fb04875ee8fc3e0fd1240d8c6edbae8dbddb992cc3a9fa89c4c6166
3
+ metadata.gz: 2300c17942818f67efb101af6443bbe021cd8ef269df1e5ab7e073abde7f04c0
4
+ data.tar.gz: b884a42aa3a35334e2fe12dcc197329f3656ce565eaeb362ec2a865a641d9aa3
5
5
  SHA512:
6
- metadata.gz: 895e1f7f17a43f5b9ec5d8fe05a71a09d3b024c55c639f4fcd4c0916597502f0ff23caf352f9001c6d3e45b6b959f134fa590576e83a24383dfc2cf53f9f6434
7
- data.tar.gz: cdb250ff046a059b5aaf59f7fcc2384ff8174e9fbdde943826d09280403f41f4bbd9f5ec7127158b5dec014f1128dfd01c7e7e93afcd11ef4004bc4e602722f2
6
+ metadata.gz: d3ee07d9718e3d8dba262056152be6a952ec3eca8b66a9c49205b150e1b122c6a2f0301a96b01acba543a043dc5e98a2576aad9b4b5b458a126f30977f094334
7
+ data.tar.gz: fd0fb4a29d2376cbf751a8c94c995eb64f40851e7ec865a33a62df9bd64e81a67de6614504ff155ea93cfe5a2375c2e6b73caa04a9e895481238b0d8598f7c8a
@@ -40,6 +40,22 @@ module Kommando
40
40
  end
41
41
  end
42
42
 
43
+ def self.metrics
44
+ executable = where('TIMEZONE(\'UTC\', NOW()) >= handle_at').
45
+ where.not("wait_for_command_ids && (SELECT array_agg(id) FROM #{table_name})").
46
+ count
47
+
48
+ scheduled = count
49
+
50
+ with_failures = where('array_length(failures, 1) > 0').count
51
+
52
+ {
53
+ kommando_executable_commands: executable,
54
+ kommando_scheduled_commands: scheduled,
55
+ kommando_scheduled_commands_with_failures: with_failures,
56
+ }
57
+ end
58
+
43
59
  def parameters
44
60
  @parameters ||= super.deep_symbolize_keys!
45
61
  end
@@ -0,0 +1,89 @@
1
+ require 'ostruct'
2
+
3
+ # stravid@2022-01-10: This implementation is currently not threadsafe.
4
+ # Instead of two arrays we need to use a single one in
5
+ # combination with a mutex.
6
+ module Kommando
7
+ module ScheduledCommandAdapters
8
+ class Memory
9
+ @@scheduled = []
10
+ @@locked_ids = []
11
+
12
+ def self.schedule!(command, parameters, handle_at)
13
+ @@scheduled << {
14
+ id: parameters.fetch(:command_id),
15
+ name: command,
16
+ parameters: parameters,
17
+ handle_at: handle_at,
18
+ failures: [],
19
+ wait_for_command_ids: parameters.fetch(:wait_for_command_ids, []),
20
+ }
21
+ end
22
+
23
+ def self.fetch!(&block)
24
+ scheduled_ids = @@scheduled.map { |command| command[:id] }
25
+
26
+ record = @@scheduled.select do |command|
27
+ next if @@locked_ids.include?(command[:id])
28
+ next if Time.now < command[:handle_at]
29
+ next if command[:wait_for_command_ids].any? { |id| scheduled_ids.include?(id) }
30
+
31
+ true
32
+ end.sort do |a, b|
33
+ a[:handle_at] <=> b[:handle_at]
34
+ end.first
35
+
36
+ if record
37
+ @@locked_ids.push(record[:id])
38
+ result = block.call(record[:name], record[:parameters])
39
+
40
+ if result.success?
41
+ @@scheduled.delete(record)
42
+ else
43
+ record[:failures] = record[:failures].append(result.error)
44
+ record[:handle_at] = record[:handle_at] + 5 * 60
45
+ end
46
+
47
+ @@locked_ids.delete(record[:id])
48
+ end
49
+ end
50
+
51
+ def self.metrics
52
+ scheduled_ids = @@scheduled.map { |command| command[:id] }
53
+
54
+ executable = @@scheduled.select do |command|
55
+ next if @@locked_ids.include?(command[:id])
56
+ next if Time.now < command[:handle_at]
57
+ next if command[:wait_for_command_ids].any? { |id| scheduled_ids.include?(id) }
58
+
59
+ true
60
+ end.size
61
+
62
+ scheduled = count
63
+
64
+ with_failures = @@scheduled.select do |command|
65
+ command[:failures].size > 0
66
+ end.size
67
+
68
+ {
69
+ kommando_executable_commands: executable,
70
+ kommando_scheduled_commands: scheduled,
71
+ kommando_scheduled_commands_with_failures: with_failures,
72
+ }
73
+ end
74
+
75
+ def self.first
76
+ OpenStruct.new(@@scheduled.first)
77
+ end
78
+
79
+ def self.count
80
+ @@scheduled.size
81
+ end
82
+
83
+ def self.clear
84
+ @@scheduled = []
85
+ @@locked_ids = []
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,96 @@
1
+ require 'sequel'
2
+
3
+ module Kommando
4
+ module ScheduledCommandAdapters
5
+ Sequel::Model.db.extension :pg_array
6
+ Sequel::Model.db.extension :pg_json
7
+
8
+ class Sequel < ::Sequel::Model(:kommando_scheduled_commands)
9
+ unrestrict_primary_key
10
+
11
+ def self.schedule!(command, parameters, handle_at)
12
+ create({
13
+ id: parameters.fetch(:command_id),
14
+ name: command,
15
+ parameters: JSON.generate(parameters),
16
+ handle_at: handle_at,
17
+ failures: ::Sequel.pg_array([], :json),
18
+ wait_for_command_ids: ::Sequel.pg_array(parameters.fetch(:wait_for_command_ids, []), :uuid),
19
+ })
20
+ end
21
+
22
+ def self.fetch!(&block)
23
+ db.transaction do
24
+ record = lock_style("FOR UPDATE OF #{table_name} SKIP LOCKED").
25
+ where(::Sequel.lit('TIMEZONE(\'UTC\', NOW()) >= handle_at')).
26
+ exclude(::Sequel.lit("wait_for_command_ids && (SELECT array_agg(id) FROM #{table_name})")).
27
+ order(::Sequel.asc(:handle_at)).
28
+ limit(1).
29
+ first
30
+
31
+ if record
32
+ result = block.call(record.name, record.parameters)
33
+
34
+ if result.success?
35
+ record.destroy
36
+ else
37
+ failures = record.failures.append(result.error).map do |failure|
38
+ JSON.generate(failure)
39
+ end
40
+
41
+ record.update({
42
+ failures: ::Sequel.pg_array(failures, :json),
43
+ handle_at: (record.handle_at + 5 * 60).getutc,
44
+ })
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.metrics
51
+ executable = where(::Sequel.lit('TIMEZONE(\'UTC\', NOW()) >= handle_at')).
52
+ exclude(::Sequel.lit("wait_for_command_ids && (SELECT array_agg(id) FROM #{table_name})")).
53
+ count
54
+
55
+ scheduled = count
56
+
57
+ with_failures = where(::Sequel.lit('array_length(failures, 1) > 0')).count
58
+
59
+ {
60
+ kommando_executable_commands: executable,
61
+ kommando_scheduled_commands: scheduled,
62
+ kommando_scheduled_commands_with_failures: with_failures,
63
+ }
64
+ end
65
+
66
+ def parameters
67
+ @parameters ||= deep_symbolize(super)
68
+ end
69
+
70
+ def failures
71
+ @failures ||= deep_symbolize(super)
72
+ end
73
+
74
+ def handle_at
75
+ offset = super.gmt_offset
76
+ super.dup.gmtime + offset
77
+ end
78
+
79
+ private
80
+ def deep_symbolize(original)
81
+ case original
82
+ when ::Sequel::Postgres::PGArray, Array
83
+ original.map do |item|
84
+ deep_symbolize(item)
85
+ end
86
+ when ::Sequel::Postgres::JSONHash, Hash
87
+ original.map do |key, value|
88
+ [key.to_sym, deep_symbolize(value)]
89
+ end.to_h
90
+ else
91
+ original
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,25 +1,66 @@
1
1
  module Kommando
2
2
  class ScheduledCommandRunner
3
- def initialize(coordinator, adapter, dependencies)
4
- @coordinator = coordinator
3
+ JITTER = 0.5
4
+ PAUSE = 5.0
5
+
6
+ def initialize(number, adapter, dependencies)
7
+ @number = number
5
8
  @adapter = adapter
6
9
  @dependencies = dependencies
10
+ @thread = nil
11
+ @stop_before_next_fetch = false
12
+ @stopped = false
13
+ end
14
+
15
+ def start
16
+ @thread ||= start_thread("runner-#{@number}", &method(:run))
17
+ end
18
+
19
+ def stop_before_next_fetch
20
+ @stop_before_next_fetch = true
21
+ end
22
+
23
+ def stopped?
24
+ @stopped
25
+ end
26
+
27
+ def kill
28
+ @thread.kill
29
+ end
30
+
31
+ private
32
+ def start_thread(name, &block)
33
+ Thread.new do
34
+ yield
35
+ end
7
36
  end
8
37
 
9
- def call
10
- fetched_command = false
38
+ def run
39
+ fetch until @stop_before_next_fetch
40
+ @stopped = true
41
+ rescue StandardError => exception
42
+ @stopped = true
43
+ raise exception
44
+ end
45
+
46
+ def fetch
47
+ immediately_fetch_again = false
11
48
 
12
49
  @adapter.fetch! do |command_name, parameters|
13
50
  unless self.class.const_defined?(command_name)
14
51
  raise Command::UnknownCommandError, "Unknown command `#{command_name}`"
15
52
  end
16
53
 
17
- fetched_command = true
54
+ immediately_fetch_again = true
18
55
 
19
56
  self.class.const_get(command_name).execute(@dependencies, parameters)
20
57
  end
21
58
 
22
- @coordinator.schedule_next_run(fetched_command ? 0 : 5)
59
+ sleep sleep_duration_after_empty_fetch unless immediately_fetch_again
60
+ end
61
+
62
+ def sleep_duration_after_empty_fetch
63
+ rand((PAUSE - JITTER)..(PAUSE + JITTER))
23
64
  end
24
65
  end
25
66
  end
@@ -1,23 +1,37 @@
1
1
  module Kommando
2
2
  class ScheduledCommandWorker
3
- def initialize(adapter, dependencies, number_of_threads)
4
- @adapter = adapter
5
- @dependencies = dependencies
6
- @number_of_threads = number_of_threads
3
+ WAIT_STEP_SIZE = 0.25
4
+ WAIT_LIMIT = 20.0
5
+
6
+ def initialize(adapter, dependencies, number_of_runners)
7
+ @number_of_runners = number_of_runners
8
+ @runners = []
9
+
10
+ @runners << ScheduledCommandRunner.new(@runners.size + 1, adapter, dependencies) while @runners.size < @number_of_runners
11
+
12
+ @self_read, @self_write = IO.pipe
7
13
  end
8
14
 
9
15
  def start
10
- @pool = Concurrent::FixedThreadPool.new(@number_of_threads)
11
- @number_of_threads.times { schedule_next_run(0) }
12
- @pool.wait_for_termination
16
+ @runners.each(&:start)
17
+ IO.select([@self_read])
13
18
  end
14
19
 
15
20
  def stop
16
- @pool.shutdown
21
+ @runners.each(&:stop_before_next_fetch)
22
+
23
+ deadline = now + WAIT_LIMIT
24
+
25
+ sleep WAIT_STEP_SIZE until @runners.all?(&:stopped?) || now > deadline
26
+
27
+ @runners.reject(&:stopped?).each(&:kill)
28
+
29
+ @self_write.puts(:stop)
17
30
  end
18
31
 
19
- def schedule_next_run(delay)
20
- Concurrent::ScheduledTask.execute(delay, { executor: @pool }, &ScheduledCommandRunner.new(self, @adapter, @dependencies).method(:call))
32
+ private
33
+ def now
34
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
21
35
  end
22
36
  end
23
37
  end
@@ -0,0 +1,2 @@
1
+ require_relative '../kommando'
2
+ require_relative './scheduled_command_adapters/sequel'
@@ -1,3 +1,3 @@
1
1
  module Kommando
2
- VERSION = '1.0.2'
2
+ VERSION = '1.1.0'
3
3
  end
data/lib/kommando.rb CHANGED
@@ -7,6 +7,7 @@ require_relative './kommando/command_plugins/execute'
7
7
  require_relative './kommando/command_plugins/schedule'
8
8
  require_relative './kommando/command_plugins/validate'
9
9
  require_relative './kommando/command_plugins/auto_schedule'
10
+ require_relative './kommando/scheduled_command_adapters/memory'
10
11
 
11
12
  module Kommando
12
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: edgycircle_kommando
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Strauß
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '5.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sequel
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '5.0'
97
111
  description: Command architecture building blocks.
98
112
  email:
99
113
  - david.strauss@edgycircle.com
@@ -113,8 +127,11 @@ files:
113
127
  - lib/kommando/command_plugins/validate.rb
114
128
  - lib/kommando/command_result.rb
115
129
  - lib/kommando/scheduled_command_adapters/active_record.rb
130
+ - lib/kommando/scheduled_command_adapters/memory.rb
131
+ - lib/kommando/scheduled_command_adapters/sequel.rb
116
132
  - lib/kommando/scheduled_command_runner.rb
117
133
  - lib/kommando/scheduled_command_worker.rb
134
+ - lib/kommando/sequel.rb
118
135
  - lib/kommando/version.rb
119
136
  homepage: https://github.com/edgycircle/kommando
120
137
  licenses:
@@ -128,7 +145,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
128
145
  requirements:
129
146
  - - ">="
130
147
  - !ruby/object:Gem::Version
131
- version: 2.7.0
148
+ version: 2.5.0
132
149
  required_rubygems_version: !ruby/object:Gem::Requirement
133
150
  requirements:
134
151
  - - ">="