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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abf6e6cece7b22f0d5aad026d0d08bb4d8e8f9cb711616e8adcd14e0437995e0
4
- data.tar.gz: 56f7d508538e8349ee212b280d5828142934d88d16edd8d730d62909fd80780e
3
+ metadata.gz: 9d0ca54718a635b6c32268bcb6ed480683d038923eb5f62cda38e91e3d43356a
4
+ data.tar.gz: aa5d62ef5ecf4003391485e7392bc6493035a50ce73ca852fc4fa26619b17b2d
5
5
  SHA512:
6
- metadata.gz: e738ec527622411944b8582ad9210b2a093bb71b55c9c6cb1a7a3005416bca62b388e5350f43656e9c69ab1bf8642a7d1e55f09999ef8c42341e4253bfbb0b59
7
- data.tar.gz: ce2badf5898fe3457145f89465b22158d6955e0d808eb3556f551f8f37b62922ddd13fa918f9c40ce4ecfd82204b66d50909a38c1a582b0b9ccd51469a185642
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
- ## Update from v0.3.x
28
+ ## Breaking Changes
29
29
 
30
- ### Create crono_trigger system tables
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
- ### Add `locked_by:string` column to CronoTrigger::Schedulable model
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
- ues `crono_trigger` command.
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.
@@ -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"
@@ -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
 
@@ -0,0 +1,6 @@
1
+ module CronoTrigger
2
+ module Events
3
+ MONITOR = -"monitor.crono_trigger"
4
+ PROCESS_RECORD = -"process_record.crono_trigger"
5
+ end
6
+ end
@@ -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
- record.class.connection_pool.with_connection do
81
- @logger.info "(executor-thread-#{Thread.current.object_id}) Execute #{record.class}-#{record.id}"
82
- record.do_execute
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
- base = [now, self[crono_trigger_column_name(:started_at)]].compact.max
299
- cron_now = tz ? base.in_time_zone(tz) : base
300
- calculated = Chrono::NextTime.new(now: cron_now, source: self[crono_trigger_column_name(:cron)]).to_time
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?
@@ -1,3 +1,3 @@
1
1
  module CronoTrigger
2
- VERSION = "0.6.4"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -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: :desc)
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|
@@ -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 = @executor.scheduled_task_count
101
- worker_record.current_queue_size = @execution_counter.value
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
@@ -1,5 +1,10 @@
1
1
  .content {
2
2
  display: flex;
3
- justify-content: center;
3
+ justify-content: space-between;
4
4
  align-items: center;
5
+ overflow-x: scroll;
6
+ padding: 15px;
7
+ }
8
+ .content:before, .content:after {
9
+ content: "";
5
10
  }
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 { Link, Route } from "react-router-dom";
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: any) {
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
- <Route path="/workers" render={this.workersTitleRender} />
74
- <Route path="/signals" render={this.signalsTitleRender} />
75
- <Route path="/models/:name" render={this.schedulableRecordsTitleRender} />
76
- <Route exact={true} path="/models" render={this.modelsTitleRender} />
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" style={{"padding": "15px"}}>
81
- <Route path="/workers" component={Workers} />
82
- <Route path="/signals" component={Signals} />
83
- <Route path="/models/:name" render={this.schedulableRecordsRender} />
84
- <Route exact={true} path="/models" component={Models} />
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
  );
@@ -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
- that.setState(data);
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: any;
21
- private executionFetchLoop: any;
20
+ private fetchLoop: ReturnType<typeof setTimeout>;
21
+ private executionFetchLoop: ReturnType<typeof setTimeout>;
22
22
 
23
- private handleTimeRangeFilterChange = debounce((event: any) => {
24
- this.setState({timeRangeMinute: parseInt(event.target.value, 10)});
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.fetchLoop = setTimeout(() => {
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
- that.setState({executions: data.records});
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: any) => {
162
+ private wrappedHandleTimeRangeFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
160
163
  event.persist();
161
164
  this.handleTimeRangeFilterChange(event);
162
165
  }
@@ -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: any;
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
- that.setState(data);
44
+ this.setState(data);
46
45
  }).catch((err) => {
47
46
  console.error(err);
48
47
  });
@@ -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: any;
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.0f826673.css",
3
- "main.css.map": "static/css/main.0f826673.css.map",
4
- "main.js": "static/js/main.a4709ab6.js",
5
- "main.js.map": "static/js/main.a4709ab6.js.map"
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","ed32b996cce5c73ad431f419b81efc62"],["<%= URI.parse(url('/')).path.chop %>/static/css/main.0f826673.css","788194505fea667f22e40ffdaa6d825c"],["<%= URI.parse(url('/')).path.chop %>/static/js/main.a4709ab6.js","d9cf9258c1ed0d01b704015a83d0505f"]],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)}))}});
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":""}