crono_trigger 0.6.4 → 0.7.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: 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":""}