crono_trigger 0.6.3 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +7 -5
- 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
|
|
@@ -56,9 +56,9 @@ module CronoTrigger
|
|
56
56
|
@logger.info "(polling-thread-#{Thread.current.object_id}) Poll #{model}"
|
57
57
|
|
58
58
|
maybe_has_next = true
|
59
|
-
while maybe_has_next
|
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":""}
|