crono_trigger 0.6.4 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +43 -4
- data/crono_trigger.gemspec +2 -2
- data/lib/crono_trigger/cli.rb +5 -1
- data/lib/crono_trigger/events.rb +6 -0
- data/lib/crono_trigger/polling_thread.rb +6 -4
- data/lib/crono_trigger/schedulable.rb +26 -3
- data/lib/crono_trigger/version.rb +1 -1
- data/lib/crono_trigger/web.rb +1 -1
- data/lib/crono_trigger/worker.rb +46 -2
- data/lib/crono_trigger.rb +2 -0
- data/lib/generators/crono_trigger/model/templates/model.rb +2 -0
- data/web/app/src/App.css +6 -1
- data/web/app/src/App.tsx +38 -36
- data/web/app/src/Models.tsx +1 -2
- data/web/app/src/SchedulableRecords.tsx +11 -8
- data/web/app/src/Signals.tsx +2 -3
- data/web/app/src/Workers.tsx +1 -1
- data/web/public/asset-manifest.json +4 -4
- data/web/public/service-worker.js +1 -1
- data/web/public/static/css/main.4eb0b8e2.css +2 -0
- data/web/public/static/css/main.4eb0b8e2.css.map +1 -0
- data/web/public/static/js/main.a59b5909.js +2 -0
- data/web/public/static/js/main.a59b5909.js.map +1 -0
- data/web/views/index.erb +1 -1
- metadata +12 -11
- data/web/public/static/css/main.0f826673.css +0 -2
- data/web/public/static/css/main.0f826673.css.map +0 -1
- data/web/public/static/js/main.a4709ab6.js +0 -2
- data/web/public/static/js/main.a4709ab6.js.map +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d0ca54718a635b6c32268bcb6ed480683d038923eb5f62cda38e91e3d43356a
|
4
|
+
data.tar.gz: aa5d62ef5ecf4003391485e7392bc6493035a50ce73ca852fc4fa26619b17b2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54ed50aac9d11ae950e1cad9651a8a4c27a5a07f169eb2c92c8f2c761420f9e6ed2a5b83965df91e7e14e67128ee6a0698f5a6a9d5d6566946b8544f8795dcb0
|
7
|
+
data.tar.gz: 10422746cbf88079591790766fd4e58f9152787e7a9ead6a0f94dbbd286cfc4caab8f69c6547716143db6584c7d6ab5e571435013d17f2ed1e833f77502d745d
|
data/README.md
CHANGED
@@ -25,15 +25,29 @@ Or install it yourself as:
|
|
25
25
|
|
26
26
|
$ gem install crono_trigger
|
27
27
|
|
28
|
-
##
|
28
|
+
## Breaking Changes
|
29
29
|
|
30
|
-
###
|
30
|
+
### Update from v0.6.x
|
31
|
+
|
32
|
+
In previous version, now is `2023-06-07T18:00:00+00:00`, a cron definition is `0 1 * * * *`, and `started_at` is `2023-06-08T1:00:00+00:00`.
|
33
|
+
In this case, `next_execute_at` was `2023-06-09T1:00:00+00:00`
|
34
|
+
|
35
|
+
From v0.7.0, if the cron definition and `started_at` match, include the time of `started_at` as the `next_execute_at`.
|
36
|
+
|
37
|
+
For example, now is `2023-06-07T18:00:00+00:00`, a cron definition is `0 1 * * * *`, and `started_at` is `2023-06-08T1:00:00+00:00`.
|
38
|
+
In this case, `next_execute_at` is `2023-06-08T1:00:00+00:00`.
|
39
|
+
|
40
|
+
If the current time is past `started_at`, the `next_execute_at` is based on the current time.
|
41
|
+
|
42
|
+
### Update from v0.3.x
|
43
|
+
|
44
|
+
#### Create crono_trigger system tables
|
31
45
|
```
|
32
46
|
$ rails g crono_trigger:install # => create migrations
|
33
47
|
$ rake db:migrate
|
34
48
|
```
|
35
49
|
|
36
|
-
|
50
|
+
#### Add `locked_by:string` column to CronoTrigger::Schedulable model
|
37
51
|
```
|
38
52
|
$ rails g migration add_locked_by_column_to_your_model
|
39
53
|
$ rake db:migrate
|
@@ -137,7 +151,7 @@ mail.next_execute_at # => next 13:00 with Asia/Japan
|
|
137
151
|
|
138
152
|
#### Run Worker
|
139
153
|
|
140
|
-
|
154
|
+
use `crono_trigger` command.
|
141
155
|
`crono_trigger` command accepts model class names.
|
142
156
|
|
143
157
|
For example,
|
@@ -156,6 +170,7 @@ Usage: crono_trigger [options] MODEL [MODEL..]
|
|
156
170
|
-p, --polling-thread=SIZE Polling thread size (Default: 1)
|
157
171
|
-i, --polling-interval=SECOND Polling interval seconds (Default: 5)
|
158
172
|
-c, --concurrency=SIZE Execute thread size (Default: 25)
|
173
|
+
-r, --fetch-records=SIZE Record count fetched by polling thread (Default: concurrency * 3)
|
159
174
|
-l, --log=LOGFILE Set log output destination (Default: STDOUT or ./crono_trigger.log if daemonize is true)
|
160
175
|
--log-level=LEVEL Set log level (Default: info)
|
161
176
|
-d, --daemonize Daemon mode
|
@@ -207,6 +222,30 @@ mount CronoTrigger::Web => '/crono_trigger'
|
|
207
222
|
This gem has rollbar plugin.
|
208
223
|
If `crono_trigger/rollbar` is required, Add Rollbar logging process to `CronoTrigger.config.error_handlers`
|
209
224
|
|
225
|
+
## Active Support Instrumentation Events
|
226
|
+
|
227
|
+
This gem provides the following events for [Active Support Instrumentation](https://guides.rubyonrails.org/active_support_instrumentation.html).
|
228
|
+
|
229
|
+
### monitor.crono\_trigger
|
230
|
+
|
231
|
+
This event is triggered every 20 seconds by the first active worker in worker_id order, so note that other workers don't receive the event.
|
232
|
+
|
233
|
+
| Key | Value |
|
234
|
+
| ------------------------ | ----------------------------------------------------------------------------- |
|
235
|
+
| model\_name | The model name |
|
236
|
+
| executable\_count | The number of executable records |
|
237
|
+
| max\_lock\_duration\_sec | The maximum amount of time since locked records started being processed |
|
238
|
+
| max\_latency\_sec | The maximum amount of time since executable records got ready to be processed |
|
239
|
+
|
240
|
+
|
241
|
+
### process\_record.crono\_trigger
|
242
|
+
|
243
|
+
This event is triggered every time a record finishes being processed.
|
244
|
+
|
245
|
+
| Key | Value |
|
246
|
+
| ------- | -------------------- |
|
247
|
+
| record | The processed record |
|
248
|
+
|
210
249
|
## Development
|
211
250
|
|
212
251
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/crono_trigger.gemspec
CHANGED
@@ -22,9 +22,9 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
24
|
|
25
|
-
spec.add_dependency "chrono"
|
25
|
+
spec.add_dependency "chrono", ">= 0.6.0"
|
26
26
|
spec.add_dependency "serverengine"
|
27
|
-
spec.add_dependency "concurrent-ruby"
|
27
|
+
spec.add_dependency "concurrent-ruby", ">= 1.1.10"
|
28
28
|
spec.add_dependency "tzinfo"
|
29
29
|
spec.add_dependency "sinatra"
|
30
30
|
spec.add_dependency "rack-contrib"
|
data/lib/crono_trigger/cli.rb
CHANGED
@@ -34,6 +34,10 @@ opt_parser = OptionParser.new do |opts|
|
|
34
34
|
options[:executor_thread] = i
|
35
35
|
end
|
36
36
|
|
37
|
+
opts.on("-r", "--fetch-records=SIZE", Integer, "Record count fetched by polling thread (Default: concurrency * 3)") do |i|
|
38
|
+
options[:fetch_records] = i
|
39
|
+
end
|
40
|
+
|
37
41
|
opts.on("-l", "--log=LOGFILE", "Set log output destination (Default: STDOUT or ./crono_trigger.log if daemonize is true)") do |log|
|
38
42
|
options[:log] = log
|
39
43
|
end
|
@@ -67,7 +71,7 @@ end
|
|
67
71
|
|
68
72
|
CronoTrigger.load_config(options[:config], options[:env]) if options[:config]
|
69
73
|
|
70
|
-
%i(worker_id polling_thread polling_interval executor_thread).each do |name|
|
74
|
+
%i(worker_id polling_thread polling_interval executor_thread fetch_records).each do |name|
|
71
75
|
CronoTrigger.config[name] = options[name] if options[name]
|
72
76
|
end
|
73
77
|
|
@@ -58,7 +58,7 @@ module CronoTrigger
|
|
58
58
|
maybe_has_next = true
|
59
59
|
while maybe_has_next && !@stop_flag.set?
|
60
60
|
records, maybe_has_next = model.connection_pool.with_connection do
|
61
|
-
model.executables_with_lock
|
61
|
+
model.executables_with_lock(limit: CronoTrigger.config.fetch_records || CronoTrigger.config.executor_thread * 3)
|
62
62
|
end
|
63
63
|
|
64
64
|
records.each do |record|
|
@@ -77,9 +77,11 @@ module CronoTrigger
|
|
77
77
|
private
|
78
78
|
|
79
79
|
def process_record(record)
|
80
|
-
|
81
|
-
|
82
|
-
|
80
|
+
ActiveSupport::Notifications.instrument(CronoTrigger::Events::PROCESS_RECORD, { record: record }) do
|
81
|
+
record.class.connection_pool.with_connection do
|
82
|
+
@logger.info "(executor-thread-#{Thread.current.object_id}) Execute #{record.class}-#{record.id}"
|
83
|
+
record.do_execute
|
84
|
+
end
|
83
85
|
end
|
84
86
|
rescue Exception => ex
|
85
87
|
@logger.error(ex)
|
@@ -295,9 +295,13 @@ module CronoTrigger
|
|
295
295
|
def calculate_next_execute_at(now = Time.current)
|
296
296
|
if self[crono_trigger_column_name(:cron)]
|
297
297
|
tz = self[crono_trigger_column_name(:timezone)].try { |zn| TZInfo::Timezone.get(zn) }
|
298
|
-
|
299
|
-
|
300
|
-
|
298
|
+
started_at = self[crono_trigger_column_name(:started_at)]
|
299
|
+
if started_at&.>(now)
|
300
|
+
calculated = calculate_next_execute_at_by_started_at(started_at, tz)
|
301
|
+
else
|
302
|
+
cron_now = tz ? now.in_time_zone(tz) : now
|
303
|
+
calculated = Chrono::NextTime.new(now: cron_now, source: self[crono_trigger_column_name(:cron)]).to_time
|
304
|
+
end
|
301
305
|
|
302
306
|
return calculated unless self[crono_trigger_column_name(:finished_at)]
|
303
307
|
return if calculated > self[crono_trigger_column_name(:finished_at)]
|
@@ -306,6 +310,25 @@ module CronoTrigger
|
|
306
310
|
end
|
307
311
|
end
|
308
312
|
|
313
|
+
# If the cron definition and started_at match, include the time of started_at as the next execute_at
|
314
|
+
def calculate_next_execute_at_by_started_at(started_at, timezone)
|
315
|
+
cron_now = timezone ? started_at.in_time_zone(timezone) : started_at
|
316
|
+
schedule = Chrono::Schedule.new(self[crono_trigger_column_name(:cron)])
|
317
|
+
|
318
|
+
if schedule.minutes.include?(cron_now.min) &&
|
319
|
+
schedule.hours.include?(cron_now.hour) &&
|
320
|
+
(schedule.days.include?(cron_now.day) || schedule.days.empty?) &&
|
321
|
+
schedule.months.include?(cron_now.month) &&
|
322
|
+
schedule.wdays.include?(cron_now.wday) &&
|
323
|
+
cron_now.sec == 0
|
324
|
+
|
325
|
+
# Execute job at started_at
|
326
|
+
cron_now
|
327
|
+
else
|
328
|
+
Chrono::NextTime.new(now: cron_now, source: self[crono_trigger_column_name(:cron)]).to_time
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
309
332
|
def set_current_cycle_id
|
310
333
|
if self.class.column_names.include?(crono_trigger_column_name(:current_cycle_id)) &&
|
311
334
|
self[crono_trigger_column_name(:current_cycle_id)].nil?
|
data/lib/crono_trigger/web.rb
CHANGED
@@ -113,7 +113,7 @@ module CronoTrigger
|
|
113
113
|
model_class = CronoTrigger::Schedulable.included_by.find { |c| c.name == params[:name] }
|
114
114
|
if model_class
|
115
115
|
after_minute = params[:after] ? Integer(params[:after]) : 10
|
116
|
-
@scheduled_records = model_class.executables(from: Time.now.since(after_minute.minutes), limit: 100, including_locked: true).reorder(next_execute_at
|
116
|
+
@scheduled_records = model_class.executables(from: Time.now.since(after_minute.minutes), limit: 100, including_locked: true).reorder(model_class.crono_trigger_column_name(:next_execute_at) => :desc)
|
117
117
|
@scheduled_records.where!(locked_by: params[:worker_id]) if params[:worker_id]
|
118
118
|
now = Time.now
|
119
119
|
records = @scheduled_records.map do |r|
|
data/lib/crono_trigger/worker.rb
CHANGED
@@ -6,6 +6,7 @@ module CronoTrigger
|
|
6
6
|
module Worker
|
7
7
|
HEARTBEAT_INTERVAL = 60
|
8
8
|
SIGNAL_FETCH_INTERVAL = 10
|
9
|
+
MONITOR_INTERVAL = 20
|
9
10
|
EXECUTOR_SHUTDOWN_TIMELIMIT = 300
|
10
11
|
OTHER_THREAD_SHUTDOWN_TIMELIMIT = 120
|
11
12
|
attr_reader :polling_threads
|
@@ -15,6 +16,7 @@ module CronoTrigger
|
|
15
16
|
@stop_flag = ServerEngine::BlockingFlag.new
|
16
17
|
@heartbeat_stop_flag = ServerEngine::BlockingFlag.new
|
17
18
|
@signal_fetch_stop_flag = ServerEngine::BlockingFlag.new
|
19
|
+
@monitor_stop_flag = ServerEngine::BlockingFlag.new
|
18
20
|
@model_queue = Queue.new
|
19
21
|
@model_names = CronoTrigger.config.model_names || CronoTrigger::Schedulable.included_by
|
20
22
|
@model_names.each do |model_name|
|
@@ -34,6 +36,7 @@ module CronoTrigger
|
|
34
36
|
def run
|
35
37
|
@heartbeat_thread = run_heartbeat_thread
|
36
38
|
@signal_fetcn_thread = run_signal_fetch_thread
|
39
|
+
@monitor_thread = run_monitor_thread
|
37
40
|
|
38
41
|
polling_thread_count = CronoTrigger.config.polling_thread || [@model_names.size, Concurrent.processor_count].min
|
39
42
|
# Assign local variable for Signal handling
|
@@ -63,6 +66,7 @@ module CronoTrigger
|
|
63
66
|
@stop_flag.set!
|
64
67
|
@heartbeat_stop_flag.set!
|
65
68
|
@signal_fetch_stop_flag.set!
|
69
|
+
@monitor_stop_flag.set!
|
66
70
|
end
|
67
71
|
|
68
72
|
def stopped?
|
@@ -92,13 +96,21 @@ module CronoTrigger
|
|
92
96
|
end
|
93
97
|
end
|
94
98
|
|
99
|
+
def run_monitor_thread
|
100
|
+
Thread.start do
|
101
|
+
until @monitor_stop_flag.wait_for_set(MONITOR_INTERVAL)
|
102
|
+
monitor
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
95
107
|
def heartbeat
|
96
108
|
CronoTrigger::Models::Worker.connection_pool.with_connection do
|
97
109
|
begin
|
98
110
|
worker_record = CronoTrigger::Models::Worker.find_or_initialize_by(worker_id: @crono_trigger_worker_id)
|
99
111
|
worker_record.max_thread_size = @executor.max_length
|
100
|
-
worker_record.current_executing_size = @
|
101
|
-
worker_record.current_queue_size = @
|
112
|
+
worker_record.current_executing_size = @execution_counter.value
|
113
|
+
worker_record.current_queue_size = @executor.queue_length
|
102
114
|
worker_record.executor_status = executor_status
|
103
115
|
worker_record.polling_model_names = @model_names
|
104
116
|
worker_record.last_heartbeated_at = Time.current
|
@@ -145,5 +157,37 @@ module CronoTrigger
|
|
145
157
|
rescue => ex
|
146
158
|
CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
|
147
159
|
end
|
160
|
+
|
161
|
+
def monitor
|
162
|
+
return unless ActiveSupport::Notifications.notifier.listening?(CronoTrigger::Events::MONITOR)
|
163
|
+
|
164
|
+
CronoTrigger::Models::Worker.connection_pool.with_connection do
|
165
|
+
if CronoTrigger.workers.where("polling_model_names = ?", @model_names.to_json).order(:worker_id).limit(1).pluck(:worker_id).first != @crono_trigger_worker_id
|
166
|
+
# Return immediately to avoid redundant instruments
|
167
|
+
return
|
168
|
+
end
|
169
|
+
|
170
|
+
@model_names.each do |model_name|
|
171
|
+
model = model_name.classify.constantize
|
172
|
+
executable_count = model.executables.limit(nil).count
|
173
|
+
|
174
|
+
execute_lock_column = model.crono_trigger_column_name(:execute_lock)
|
175
|
+
oldest_execute_lock = model.executables(including_locked: true).where.not(execute_lock_column => 0).order(execute_lock_column).limit(1).pluck(execute_lock_column).first
|
176
|
+
|
177
|
+
next_execute_at_column = model.crono_trigger_column_name(:next_execute_at)
|
178
|
+
oldest_next_execute_at = model.executables.order(next_execute_at_column).limit(1).pluck(next_execute_at_column).first
|
179
|
+
|
180
|
+
now = Time.now
|
181
|
+
ActiveSupport::Notifications.instrument(CronoTrigger::Events::MONITOR, {
|
182
|
+
model_name: model_name,
|
183
|
+
executable_count: executable_count,
|
184
|
+
max_lock_duration_sec: oldest_execute_lock.nil? ? 0 : now.to_i - oldest_execute_lock,
|
185
|
+
max_latency_sec: oldest_next_execute_at.nil? ? 0 : now - oldest_next_execute_at,
|
186
|
+
})
|
187
|
+
end
|
188
|
+
end
|
189
|
+
rescue => ex
|
190
|
+
CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
|
191
|
+
end
|
148
192
|
end
|
149
193
|
end
|
data/lib/crono_trigger.rb
CHANGED
@@ -4,6 +4,7 @@ require "ostruct"
|
|
4
4
|
require "socket"
|
5
5
|
require "active_record"
|
6
6
|
require "concurrent"
|
7
|
+
require "crono_trigger/events"
|
7
8
|
require "crono_trigger/models/worker"
|
8
9
|
require "crono_trigger/models/signal"
|
9
10
|
require "crono_trigger/models/execution"
|
@@ -17,6 +18,7 @@ module CronoTrigger
|
|
17
18
|
polling_thread: nil,
|
18
19
|
polling_interval: 5,
|
19
20
|
executor_thread: 25,
|
21
|
+
fetch_records: nil, # default is executor_thread * 3
|
20
22
|
model_names: nil,
|
21
23
|
error_handlers: [],
|
22
24
|
global_error_handlers: [],
|
@@ -1,5 +1,7 @@
|
|
1
1
|
<% module_namespacing do -%>
|
2
2
|
class <%= class_name %> < <%= parent_class_name.classify %>
|
3
|
+
include CronoTrigger::Schedulable
|
4
|
+
|
3
5
|
<% attributes.select(&:reference?).each do |attribute| -%>
|
4
6
|
belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
|
5
7
|
<% end -%>
|
data/web/app/src/App.css
CHANGED
data/web/app/src/App.tsx
CHANGED
@@ -6,7 +6,8 @@ import Toolbar from '@material-ui/core/Toolbar';
|
|
6
6
|
import Typography from '@material-ui/core/Typography';
|
7
7
|
import MenuIcon from '@material-ui/icons/Menu';
|
8
8
|
import * as React from 'react';
|
9
|
-
import {
|
9
|
+
import { RouteComponentProps } from "react-router";
|
10
|
+
import { Link, Route, Switch } from "react-router-dom";
|
10
11
|
import './App.css';
|
11
12
|
import Models from './Models';
|
12
13
|
import SchedulableRecords from './SchedulableRecords';
|
@@ -18,44 +19,29 @@ interface IAppState {
|
|
18
19
|
}
|
19
20
|
|
20
21
|
class App extends React.Component<any, IAppState> {
|
21
|
-
private workersTitleRender: () => JSX.Element;
|
22
|
-
private signalsTitleRender: () => JSX.Element;
|
23
|
-
private modelsTitleRender: () => JSX.Element;
|
24
|
-
private schedulableRecordsTitleRender: (props: any) => JSX.Element;
|
25
|
-
private schedulableRecordsRender: (props: any) => JSX.Element;
|
26
|
-
|
27
22
|
public constructor(props: any) {
|
28
23
|
super(props);
|
29
|
-
this.handleMenuButtonClick = this.handleMenuButtonClick.bind(this);
|
30
|
-
this.handleMenuClose = this.handleMenuClose.bind(this);
|
31
24
|
this.state = {menuAnchorEl: null};
|
32
|
-
this.workersTitleRender = () => (
|
33
|
-
<Typography variant="title" color="inherit">Workers</Typography>
|
34
|
-
)
|
35
|
-
this.signalsTitleRender = () => (
|
36
|
-
<Typography variant="title" color="inherit">Signals</Typography>
|
37
|
-
)
|
38
|
-
this.modelsTitleRender = () => (
|
39
|
-
<Typography variant="title" color="inherit">Models</Typography>
|
40
|
-
)
|
41
|
-
this.schedulableRecordsTitleRender = ({ match }) => (
|
42
|
-
<Typography variant="title" color="inherit">{match.params.name}</Typography>
|
43
|
-
)
|
44
|
-
this.schedulableRecordsRender = ({ match }) => (
|
45
|
-
<SchedulableRecords model_name={match.params.name} />
|
46
|
-
)
|
47
25
|
}
|
48
26
|
|
49
|
-
public handleMenuButtonClick(event:
|
27
|
+
public handleMenuButtonClick = (event: React.MouseEvent<HTMLElement>) => {
|
50
28
|
this.setState({menuAnchorEl: event.currentTarget});
|
51
29
|
}
|
52
30
|
|
53
|
-
public handleMenuClose() {
|
31
|
+
public handleMenuClose = () => {
|
54
32
|
this.setState({menuAnchorEl: null});
|
55
33
|
}
|
56
34
|
|
35
|
+
public schedulableRecordsTitleRender({ match }: RouteComponentProps<any>): JSX.Element {
|
36
|
+
return <Typography variant="title" color="inherit">{match.params.name}</Typography>;
|
37
|
+
}
|
38
|
+
|
39
|
+
public schedulableRecordsRender({ match }: RouteComponentProps<any>): JSX.Element {
|
40
|
+
return <SchedulableRecords model_name={match.params.name}/>;
|
41
|
+
}
|
42
|
+
|
57
43
|
public render() {
|
58
|
-
const { menuAnchorEl }= this.state;
|
44
|
+
const { menuAnchorEl } = this.state;
|
59
45
|
|
60
46
|
return (
|
61
47
|
<div className="main">
|
@@ -70,18 +56,34 @@ class App extends React.Component<any, IAppState> {
|
|
70
56
|
<MenuItem><Link to="/models" onClick={this.handleMenuClose}>Models</Link></MenuItem>
|
71
57
|
</Menu>
|
72
58
|
|
73
|
-
<
|
74
|
-
|
75
|
-
|
76
|
-
|
59
|
+
<Switch>
|
60
|
+
<Route path="/workers">
|
61
|
+
<Typography variant="title" color="inherit">Workers</Typography>
|
62
|
+
</Route>
|
63
|
+
<Route path="/signals">
|
64
|
+
<Typography variant="title" color="inherit">Signals</Typography>
|
65
|
+
</Route>
|
66
|
+
<Route path="/models/:name" render={this.schedulableRecordsTitleRender} />
|
67
|
+
<Route exact={true} path="/models">
|
68
|
+
<Typography variant="title" color="inherit">Models</Typography>
|
69
|
+
</Route>
|
70
|
+
</Switch>
|
77
71
|
</Toolbar>
|
78
72
|
</AppBar>
|
79
73
|
|
80
|
-
<div className="content"
|
81
|
-
<
|
82
|
-
|
83
|
-
|
84
|
-
|
74
|
+
<div className="content">
|
75
|
+
<Switch>
|
76
|
+
<Route path="/workers">
|
77
|
+
<Workers />
|
78
|
+
</Route>
|
79
|
+
<Route path="/signals">
|
80
|
+
<Signals />
|
81
|
+
</Route>
|
82
|
+
<Route path="/models/:name" render={this.schedulableRecordsRender} />
|
83
|
+
<Route exact={true} path="/models">
|
84
|
+
<Models />
|
85
|
+
</Route>
|
86
|
+
</Switch>
|
85
87
|
</div>
|
86
88
|
</div>
|
87
89
|
);
|
data/web/app/src/Models.tsx
CHANGED
@@ -26,11 +26,10 @@ class Models extends React.Component<any, IModelsState> {
|
|
26
26
|
}
|
27
27
|
|
28
28
|
public fetchModels(): void {
|
29
|
-
const that = this;
|
30
29
|
fetch(`${window.mountPath}/models.json`)
|
31
30
|
.then((res) => res.json())
|
32
31
|
.then((data) => {
|
33
|
-
|
32
|
+
this.setState(data);
|
34
33
|
}).catch((err) => {
|
35
34
|
console.error(err);
|
36
35
|
});
|
@@ -17,11 +17,15 @@ import SchedulableRecordTableCell from './SchedulableRecordTableCell';
|
|
17
17
|
declare var window: IGlobalWindow;
|
18
18
|
|
19
19
|
class SchedulableRecords extends React.Component<ISchedulableRecordsProps, ISchedulableRecordsStates> {
|
20
|
-
private fetchLoop:
|
21
|
-
private executionFetchLoop:
|
20
|
+
private fetchLoop: ReturnType<typeof setTimeout>;
|
21
|
+
private executionFetchLoop: ReturnType<typeof setTimeout>;
|
22
22
|
|
23
|
-
private handleTimeRangeFilterChange = debounce((event:
|
24
|
-
|
23
|
+
private handleTimeRangeFilterChange = debounce((event: React.ChangeEvent<HTMLInputElement>) => {
|
24
|
+
const inputValue = parseInt(event.target.value, 10);
|
25
|
+
if (isNaN(inputValue)) {
|
26
|
+
return;
|
27
|
+
}
|
28
|
+
this.setState({timeRangeMinute: inputValue});
|
25
29
|
this.fetchSchedulableRecord();
|
26
30
|
}, 500)
|
27
31
|
|
@@ -67,18 +71,17 @@ class SchedulableRecords extends React.Component<ISchedulableRecordsProps, ISche
|
|
67
71
|
}
|
68
72
|
|
69
73
|
public setFetchExecutionLoop(): void {
|
70
|
-
this.
|
74
|
+
this.executionFetchLoop = setTimeout(() => {
|
71
75
|
this.fetchExecution();
|
72
76
|
this.setFetchExecutionLoop();
|
73
77
|
}, 3000);
|
74
78
|
}
|
75
79
|
|
76
80
|
public fetchExecution(): void {
|
77
|
-
const that = this;
|
78
81
|
fetch(`${window.mountPath}/models/${this.props.model_name}/executions.json`)
|
79
82
|
.then((res) => res.json())
|
80
83
|
.then((data) => {
|
81
|
-
|
84
|
+
this.setState({executions: data.records});
|
82
85
|
}).catch((err) => {
|
83
86
|
console.error(err);
|
84
87
|
});
|
@@ -156,7 +159,7 @@ class SchedulableRecords extends React.Component<ISchedulableRecordsProps, ISche
|
|
156
159
|
)
|
157
160
|
}
|
158
161
|
|
159
|
-
private wrappedHandleTimeRangeFilterChange = (event:
|
162
|
+
private wrappedHandleTimeRangeFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
160
163
|
event.persist();
|
161
164
|
this.handleTimeRangeFilterChange(event);
|
162
165
|
}
|
data/web/app/src/Signals.tsx
CHANGED
@@ -12,7 +12,7 @@ import Signal from './Signal';
|
|
12
12
|
declare var window: IGlobalWindow;
|
13
13
|
|
14
14
|
class Signals extends React.Component<any, ISignalsState> {
|
15
|
-
private fetchLoop:
|
15
|
+
private fetchLoop: ReturnType<typeof setTimeout>;
|
16
16
|
|
17
17
|
constructor(props: any) {
|
18
18
|
super(props)
|
@@ -38,11 +38,10 @@ class Signals extends React.Component<any, ISignalsState> {
|
|
38
38
|
}
|
39
39
|
|
40
40
|
public fetchSignals(): void {
|
41
|
-
const that = this;
|
42
41
|
fetch(`${window.mountPath}/signals.json`)
|
43
42
|
.then((res) => res.json())
|
44
43
|
.then((data) => {
|
45
|
-
|
44
|
+
this.setState(data);
|
46
45
|
}).catch((err) => {
|
47
46
|
console.error(err);
|
48
47
|
});
|
data/web/app/src/Workers.tsx
CHANGED
@@ -12,7 +12,7 @@ import Worker from "./Worker";
|
|
12
12
|
declare var window: IGlobalWindow;
|
13
13
|
|
14
14
|
class Workers extends React.Component<any, IWorkersState> {
|
15
|
-
private fetchLoop:
|
15
|
+
private fetchLoop: ReturnType<typeof setTimeout>;
|
16
16
|
|
17
17
|
constructor(props: any) {
|
18
18
|
super(props);
|
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
|
-
"main.css": "static/css/main.
|
3
|
-
"main.css.map": "static/css/main.
|
4
|
-
"main.js": "static/js/main.
|
5
|
-
"main.js.map": "static/js/main.
|
2
|
+
"main.css": "static/css/main.4eb0b8e2.css",
|
3
|
+
"main.css.map": "static/css/main.4eb0b8e2.css.map",
|
4
|
+
"main.js": "static/js/main.a59b5909.js",
|
5
|
+
"main.js.map": "static/js/main.a59b5909.js.map"
|
6
6
|
}
|
@@ -1 +1 @@
|
|
1
|
-
"use strict";var precacheConfig=[["<%= URI.parse(url('/')).path.chop %>/index.html","
|
1
|
+
"use strict";var precacheConfig=[["<%= URI.parse(url('/')).path.chop %>/index.html","8e1a97325102ea2cb2cf9e5c974d7cc3"],["<%= URI.parse(url('/')).path.chop %>/static/css/main.4eb0b8e2.css","9b7bfec58c5bbc0f149ff2d6e3953c91"],["<%= URI.parse(url('/')).path.chop %>/static/js/main.a59b5909.js","b51b124864942c6fc4df624d957ebcb0"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(t){return t.redirected?("body"in t?Promise.resolve(t.body):t.blob()).then(function(e){return new Response(e,{headers:t.headers,status:t.status,statusText:t.statusText})}):Promise.resolve(t)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,n){var t=new URL(e);return t.hash="",t.search=t.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(t){return n.every(function(e){return!e.test(t[0])})}).map(function(e){return e.join("=")}).join("&"),t.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(r){return setOfCachedUrls(r).then(function(n){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(t){if(!n.has(t)){var e=new Request(t,{credentials:"same-origin"});return fetch(e).then(function(e){if(!e.ok)throw new Error("Request for "+t+" returned a response with status "+e.status);return cleanResponse(e).then(function(e){return r.put(t,e)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var n=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(t){return t.keys().then(function(e){return Promise.all(e.map(function(e){if(!n.has(e.url))return t.delete(e)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(t){if("GET"===t.request.method){var e,n=stripIgnoredUrlParameters(t.request.url,ignoreUrlParametersMatching),r="index.html";(e=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,r),e=urlsToCacheKeys.has(n));0,e&&t.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(e){return console.warn('Couldn\'t serve response for "%s" from cache: %O',t.request.url,e),fetch(t.request)}))}});
|
@@ -0,0 +1,2 @@
|
|
1
|
+
.content{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:center;align-items:center;overflow-x:scroll;padding:15px}.content:after,.content:before{content:""}body{margin:0;padding:0;font-family:sans-serif}
|
2
|
+
/*# sourceMappingURL=main.4eb0b8e2.css.map*/
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["App.css","index.css"],"names":[],"mappings":"AAAA,SACE,oBACA,aACA,sBACI,8BACJ,sBACI,mBACJ,kBACA,YAAc,CAEhB,+BACE,UAAY,CCXd,KACE,SACA,UACA,sBAAwB","file":"static/css/main.4eb0b8e2.css","sourcesContent":[".content {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: justify;\n justify-content: space-between;\n -ms-flex-align: center;\n align-items: center;\n overflow-x: scroll;\n padding: 15px;\n}\n.content:before, .content:after {\n content: \"\";\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/App.css","body {\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/index.css"],"sourceRoot":""}
|