crono_trigger 0.3.2 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/README.md +40 -0
  4. data/Rakefile +17 -0
  5. data/crono_trigger.gemspec +4 -1
  6. data/exe/crono_trigger-web +33 -0
  7. data/lib/crono_trigger.rb +20 -2
  8. data/lib/crono_trigger/cli.rb +8 -3
  9. data/lib/crono_trigger/models/signal.rb +52 -0
  10. data/lib/crono_trigger/models/worker.rb +16 -0
  11. data/lib/crono_trigger/polling_thread.rb +58 -20
  12. data/lib/crono_trigger/railtie.rb +15 -0
  13. data/lib/crono_trigger/schedulable.rb +69 -17
  14. data/lib/crono_trigger/version.rb +1 -1
  15. data/lib/crono_trigger/web.rb +163 -0
  16. data/lib/crono_trigger/worker.rb +118 -8
  17. data/lib/generators/crono_trigger/install/install_generator.rb +16 -0
  18. data/lib/generators/crono_trigger/install/templates/install.rb +23 -0
  19. data/lib/generators/crono_trigger/migration/templates/create_table_migration.rb +1 -0
  20. data/lib/generators/crono_trigger/migration/templates/migration.rb +1 -0
  21. data/web/app/.gitignore +21 -0
  22. data/web/app/README.md +2448 -0
  23. data/web/app/images.d.ts +3 -0
  24. data/web/app/package-lock.json +12439 -0
  25. data/web/app/package.json +36 -0
  26. data/web/app/public/favicon.ico +0 -0
  27. data/web/app/public/index.html +45 -0
  28. data/web/app/public/manifest.json +8 -0
  29. data/web/app/src/App.css +5 -0
  30. data/web/app/src/App.test.tsx +9 -0
  31. data/web/app/src/App.tsx +91 -0
  32. data/web/app/src/Models.tsx +61 -0
  33. data/web/app/src/SchedulableRecord.tsx +208 -0
  34. data/web/app/src/SchedulableRecordTableCell.tsx +19 -0
  35. data/web/app/src/SchedulableRecords.tsx +110 -0
  36. data/web/app/src/Signal.tsx +21 -0
  37. data/web/app/src/Signals.tsx +74 -0
  38. data/web/app/src/Worker.tsx +106 -0
  39. data/web/app/src/Workers.tsx +78 -0
  40. data/web/app/src/index.css +5 -0
  41. data/web/app/src/index.tsx +15 -0
  42. data/web/app/src/interfaces.ts +77 -0
  43. data/web/app/tsconfig.json +30 -0
  44. data/web/app/tsconfig.prod.json +3 -0
  45. data/web/app/tsconfig.test.json +6 -0
  46. data/web/app/tslint.json +13 -0
  47. data/web/public/asset-manifest.json +6 -0
  48. data/web/public/favicon.ico +0 -0
  49. data/web/public/manifest.json +8 -0
  50. data/web/public/service-worker.js +1 -0
  51. data/web/public/static/css/main.0f826673.css +2 -0
  52. data/web/public/static/css/main.0f826673.css.map +1 -0
  53. data/web/public/static/js/main.1413dc51.js +2 -0
  54. data/web/public/static/js/main.1413dc51.js.map +1 -0
  55. data/web/views/index.erb +1 -0
  56. data/web/views/signals.erb +9 -0
  57. data/web/views/workers.erb +9 -0
  58. metadata +89 -3
@@ -0,0 +1,19 @@
1
+ import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles';
2
+ import TableCell from '@material-ui/core/TableCell';
3
+ import * as React from 'react';
4
+
5
+ const styles = createStyles({
6
+ property: {
7
+ padding: "4px 16px 4px 8px"
8
+ }
9
+ });
10
+
11
+ interface ITableCellProps extends WithStyles<typeof styles> {
12
+ children: string | number | null | React.ReactNode | React.ReactNode[],
13
+ }
14
+
15
+ const SchedulableRecordTableCell = withStyles(styles)((props: ITableCellProps) => (
16
+ <TableCell className={props.classes.property}>{props.children}</TableCell>
17
+ ));
18
+
19
+ export default SchedulableRecordTableCell;
@@ -0,0 +1,110 @@
1
+ import FormControl from '@material-ui/core/FormControl';
2
+ import Paper from '@material-ui/core/Paper';
3
+ import Table from '@material-ui/core/Table';
4
+ import TableBody from '@material-ui/core/TableBody';
5
+ import TableHead from '@material-ui/core/TableHead';
6
+ import TableRow from '@material-ui/core/TableRow';
7
+ import TextField from '@material-ui/core/TextField';
8
+ import { debounce } from 'lodash';
9
+ import * as React from 'react';
10
+
11
+ import { IGlobalWindow, ISchedulableRecordsProps, ISchedulableRecordsStates } from './interfaces';
12
+ import SchedulableRecord from './SchedulableRecord';
13
+ import SchedulableRecordTableCell from './SchedulableRecordTableCell';
14
+
15
+ declare var window: IGlobalWindow;
16
+
17
+ class SchedulableRecords extends React.Component<ISchedulableRecordsProps, ISchedulableRecordsStates> {
18
+ private fetchLoop: any;
19
+
20
+ private handleTimeRangeFilterChange = debounce((event: any) => {
21
+ this.setState({timeRangeMinute: parseInt(event.target.value, 10)});
22
+ this.fetchSchedulableRecord();
23
+ }, 500)
24
+
25
+ constructor(props: ISchedulableRecordsProps) {
26
+ super(props);
27
+
28
+ this.state = {records: [], timeRangeMinute: 10};
29
+ }
30
+
31
+ public componentDidMount() {
32
+ this.fetchSchedulableRecord();
33
+ this.setFetchSchedulableRecordLoop();
34
+ }
35
+
36
+ public componentWillUnmount() {
37
+ if (this.fetchLoop) {
38
+ clearTimeout(this.fetchLoop);
39
+ }
40
+ }
41
+
42
+ public setFetchSchedulableRecordLoop(): void {
43
+ this.fetchLoop = setTimeout(() => {
44
+ this.fetchSchedulableRecord();
45
+ this.setFetchSchedulableRecordLoop();
46
+ }, 3000);
47
+ }
48
+
49
+ public fetchSchedulableRecord(): void {
50
+ const that = this;
51
+ fetch(`${window.mountPath}/models/${this.props.model_name}.json?after=${this.state.timeRangeMinute}`)
52
+ .then((res) => res.json())
53
+ .then((data) => {
54
+ that.setState(data);
55
+ }).catch((err) => {
56
+ console.error(err);
57
+ });
58
+ }
59
+
60
+ public render() {
61
+ return (
62
+ <div id="schedulable-models">
63
+ <FormControl className="filter-form">
64
+ <TextField
65
+ id="time-range-input"
66
+ label="Time Range"
67
+ type="number"
68
+ defaultValue={this.state.timeRangeMinute}
69
+ helperText="minute after"
70
+ margin="normal"
71
+ onChange={this.wrappedHandleTimeRangeFilterChange}
72
+ />
73
+ </FormControl>
74
+ <Paper className="models-container" style={{marginTop: "8px"}}>
75
+ <Table className="models">
76
+ <TableHead>
77
+ <TableRow>
78
+ <SchedulableRecordTableCell>Status</SchedulableRecordTableCell>
79
+ <SchedulableRecordTableCell>ID</SchedulableRecordTableCell>
80
+ <SchedulableRecordTableCell>Cron</SchedulableRecordTableCell>
81
+ <SchedulableRecordTableCell>Next Execute At</SchedulableRecordTableCell>
82
+ <SchedulableRecordTableCell>Delay Sec</SchedulableRecordTableCell>
83
+ <SchedulableRecordTableCell>Execute Lock</SchedulableRecordTableCell>
84
+ <SchedulableRecordTableCell>Time To Unlock</SchedulableRecordTableCell>
85
+ <SchedulableRecordTableCell>Last Executed At</SchedulableRecordTableCell>
86
+ <SchedulableRecordTableCell>Locked By</SchedulableRecordTableCell>
87
+ <SchedulableRecordTableCell>Last Error Time</SchedulableRecordTableCell>
88
+ <SchedulableRecordTableCell>Retry Count</SchedulableRecordTableCell>
89
+ <SchedulableRecordTableCell>Detail</SchedulableRecordTableCell>
90
+ <SchedulableRecordTableCell>Ops</SchedulableRecordTableCell>
91
+ </TableRow>
92
+ </TableHead>
93
+ <TableBody>
94
+ {this.state.records.map((record) => (
95
+ <SchedulableRecord key={record.id} model_name={this.props.model_name} record={record} />
96
+ ))}
97
+ </TableBody>
98
+ </Table>
99
+ </Paper>
100
+ </div>
101
+ )
102
+ }
103
+
104
+ private wrappedHandleTimeRangeFilterChange = (event: any) => {
105
+ event.persist();
106
+ this.handleTimeRangeFilterChange(event);
107
+ }
108
+ }
109
+
110
+ export default SchedulableRecords;
@@ -0,0 +1,21 @@
1
+ import TableCell from '@material-ui/core/TableCell';
2
+ import TableRow from '@material-ui/core/TableRow';
3
+ import * as React from 'react';
4
+
5
+ import { ISignalProps } from './interfaces';
6
+
7
+ class Signal extends React.Component<ISignalProps, any> {
8
+ public render() {
9
+ const signal = this.props.signal;
10
+ return (
11
+ <TableRow>
12
+ <TableCell>{signal.worker_id}</TableCell>
13
+ <TableCell>{signal.signal}</TableCell>
14
+ <TableCell>{signal.sent_at}</TableCell>
15
+ <TableCell>{signal.received_at}</TableCell>
16
+ </TableRow>
17
+ )
18
+ }
19
+ }
20
+
21
+ export default Signal;
@@ -0,0 +1,74 @@
1
+ import Paper from '@material-ui/core/Paper';
2
+ import Table from '@material-ui/core/Table';
3
+ import TableBody from '@material-ui/core/TableBody';
4
+ import TableCell from '@material-ui/core/TableCell';
5
+ import TableHead from '@material-ui/core/TableHead';
6
+ import TableRow from '@material-ui/core/TableRow';
7
+ import * as React from 'react';
8
+
9
+ import { IGlobalWindow, ISignalsState } from './interfaces';
10
+ import Signal from './Signal';
11
+
12
+ declare var window: IGlobalWindow;
13
+
14
+ class Signals extends React.Component<any, ISignalsState> {
15
+ private fetchLoop: any;
16
+
17
+ constructor(props: any) {
18
+ super(props)
19
+ this.state = {records: []}
20
+ }
21
+
22
+ public componentDidMount() {
23
+ this.fetchSignals();
24
+ this.setFetchSignalLoop();
25
+ }
26
+
27
+ public componentWillUnmount() {
28
+ if (this.fetchLoop) {
29
+ clearTimeout(this.fetchLoop);
30
+ }
31
+ }
32
+
33
+ public setFetchSignalLoop(): void {
34
+ this.fetchLoop = setTimeout(() => {
35
+ this.fetchSignals();
36
+ this.setFetchSignalLoop();
37
+ }, 3000);
38
+ }
39
+
40
+ public fetchSignals(): void {
41
+ const that = this;
42
+ fetch(`${window.mountPath}/signals.json`)
43
+ .then((res) => res.json())
44
+ .then((data) => {
45
+ that.setState(data);
46
+ }).catch((err) => {
47
+ console.error(err);
48
+ });
49
+ }
50
+
51
+ public render() {
52
+ return (
53
+ <Paper className="signals-container">
54
+ <Table className="signals">
55
+ <TableHead>
56
+ <TableRow>
57
+ <TableCell>Worker ID</TableCell>
58
+ <TableCell>Signal</TableCell>
59
+ <TableCell>Sent At</TableCell>
60
+ <TableCell>Received At</TableCell>
61
+ </TableRow>
62
+ </TableHead>
63
+ <TableBody>
64
+ {this.state.records.map((record) => {
65
+ return <Signal key={`${record.worker_id}-${record.sent_at}`} signal={record} />
66
+ })}
67
+ </TableBody>
68
+ </Table>
69
+ </Paper>
70
+ )
71
+ }
72
+ }
73
+
74
+ export default Signals;
@@ -0,0 +1,106 @@
1
+ import Button from '@material-ui/core/Button';
2
+ import Snackbar from '@material-ui/core/Snackbar';
3
+ import TableCell from '@material-ui/core/TableCell';
4
+ import TableRow from '@material-ui/core/TableRow';
5
+ import * as React from 'react';
6
+
7
+ import { IGlobalWindow, IWorkerProps } from "./interfaces";
8
+
9
+ declare var window: IGlobalWindow;
10
+
11
+ class Worker extends React.Component<IWorkerProps, any> {
12
+ constructor(props: IWorkerProps) {
13
+ super(props)
14
+ this.handleQuietClick = this.handleQuietClick.bind(this)
15
+ this.handleStopClick = this.handleStopClick.bind(this)
16
+ this.handleNotificationClose = this.handleNotificationClose.bind(this)
17
+ this.state = {
18
+ notificationMessage: <span />,
19
+ notificationOpen: false,
20
+ }
21
+ }
22
+
23
+ public render() {
24
+ const worker = this.props.worker;
25
+ return (
26
+ <TableRow>
27
+ <TableCell>{worker.worker_id}</TableCell>
28
+ <TableCell numeric={true}>{worker.max_thread_size}</TableCell>
29
+ <TableCell numeric={true}>{worker.current_queue_size}</TableCell>
30
+ <TableCell numeric={true}>{worker.current_executing_size}</TableCell>
31
+ <TableCell>{worker.executor_status}</TableCell>
32
+ <TableCell>{worker.polling_model_names}</TableCell>
33
+ <TableCell>{worker.last_heartbeated_at}</TableCell>
34
+ <TableCell>
35
+ <Button variant="contained" style={{"marginRight": "8px"}} onClick={this.handleQuietClick}>Quiet</Button>
36
+ <Button variant="contained" color="secondary" onClick={this.handleStopClick}>Stop</Button>
37
+ <Snackbar
38
+ anchorOrigin={{vertical: "bottom", horizontal: "right"}}
39
+ open={this.state.notificationOpen}
40
+ autoHideDuration={3000}
41
+ onClose={this.handleNotificationClose}
42
+ message={this.state.notificationMessage}
43
+ />
44
+ </TableCell>
45
+
46
+ </TableRow>
47
+ )
48
+ }
49
+
50
+ private handleQuietClick(event: any) {
51
+ const worker = this.props.worker;
52
+ fetch(`${window.mountPath}/signals`, {
53
+ body: JSON.stringify({"worker_id": worker.worker_id, "signal": "TSTP"}),
54
+ headers: {"content-type": "application/json"},
55
+ method: "POST"
56
+ }).then(this.handleResponseStatus).then((res) => {
57
+ this.setState({
58
+ notificationMessage: <span>Quiet {worker.worker_id}</span>,
59
+ notificationOpen: true,
60
+ })
61
+ }).catch((err) => {
62
+ this.setState({
63
+ notificationMessage: <span>Failed to Quiet ({err.message})</span>,
64
+ notificationOpen: true,
65
+ })
66
+ })
67
+ }
68
+
69
+ private handleStopClick(event: any) {
70
+ const worker = this.props.worker;
71
+ fetch(`${window.mountPath}/signals`, {
72
+ body: JSON.stringify({"worker_id": worker.worker_id, "signal": "TERM"}),
73
+ headers: {"content-type": "application/json"},
74
+ method: "POST"
75
+ }).then(this.handleResponseStatus).then((res) => {
76
+ this.setState({
77
+ notificationMessage: <span>Stop {worker.worker_id}</span>,
78
+ notificationOpen: true,
79
+ })
80
+ }).catch((err) => {
81
+ this.setState({
82
+ notificationMessage: <span>Failed to Stop ({err.message})</span>,
83
+ notificationOpen: true,
84
+ })
85
+ })
86
+ }
87
+
88
+ private handleResponseStatus(res: Response): Promise<Response | Error> {
89
+ if (!res.ok) {
90
+ return res.json().then((data: any) => {
91
+ throw new Error(data.error);
92
+ })
93
+ } else {
94
+ return Promise.resolve(res);
95
+ }
96
+ }
97
+
98
+ private handleNotificationClose(ev: any, reason: any) {
99
+ this.setState({
100
+ ...this.state,
101
+ notificationOpen: false,
102
+ })
103
+ }
104
+ }
105
+
106
+ export default Worker;
@@ -0,0 +1,78 @@
1
+ import Paper from '@material-ui/core/Paper';
2
+ import Table from '@material-ui/core/Table';
3
+ import TableBody from '@material-ui/core/TableBody';
4
+ import TableCell from '@material-ui/core/TableCell';
5
+ import TableHead from '@material-ui/core/TableHead';
6
+ import TableRow from '@material-ui/core/TableRow';
7
+ import * as React from 'react';
8
+
9
+ import { IGlobalWindow, IWorkersState } from "./interfaces";
10
+ import Worker from "./Worker";
11
+
12
+ declare var window: IGlobalWindow;
13
+
14
+ class Workers extends React.Component<any, IWorkersState> {
15
+ private fetchLoop: any;
16
+
17
+ constructor(props: any) {
18
+ super(props);
19
+ this.state = {records: []};
20
+ }
21
+
22
+ public componentDidMount() {
23
+ this.fetchWorkers();
24
+ this.setFetchWorkerLoop();
25
+ }
26
+
27
+ public componentWillUnmount() {
28
+ if (this.fetchLoop) {
29
+ clearTimeout(this.fetchLoop);
30
+ }
31
+ }
32
+
33
+ public setFetchWorkerLoop(): void {
34
+ this.fetchLoop = setTimeout(() => {
35
+ this.fetchWorkers();
36
+ this.setFetchWorkerLoop();
37
+ }, 3000);
38
+ }
39
+
40
+ public fetchWorkers(): void {
41
+ const that = this;
42
+ fetch(`${window.mountPath}/workers.json`)
43
+ .then((res) => res.json())
44
+ .then((data) => {
45
+ that.setState(data);
46
+ }).catch((err) => {
47
+ console.error(err);
48
+ });
49
+ }
50
+
51
+ public render() {
52
+ return (
53
+ <Paper className="workers-container">
54
+ <Table className="workers">
55
+ <TableHead>
56
+ <TableRow>
57
+ <TableCell>Worker ID</TableCell>
58
+ <TableCell numeric={true}>Thread</TableCell>
59
+ <TableCell numeric={true}>Internal Queue</TableCell>
60
+ <TableCell numeric={true}>Executing</TableCell>
61
+ <TableCell>Status</TableCell>
62
+ <TableCell>Polling Models</TableCell>
63
+ <TableCell>Last Heatbeated At</TableCell>
64
+ <TableCell>&nbsp;</TableCell>
65
+ </TableRow>
66
+ </TableHead>
67
+ <TableBody>
68
+ {this.state.records.map((record) => {
69
+ return <Worker key={record.worker_id} worker={record} />
70
+ })}
71
+ </TableBody>
72
+ </Table>
73
+ </Paper>
74
+ )
75
+ }
76
+ }
77
+
78
+ export default Workers;
@@ -0,0 +1,5 @@
1
+ body {
2
+ margin: 0;
3
+ padding: 0;
4
+ font-family: sans-serif;
5
+ }
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom';
3
+ import { BrowserRouter as Router } from "react-router-dom";
4
+ import App from './App';
5
+ import './index.css';
6
+ import { IGlobalWindow } from './interfaces';
7
+
8
+ declare var window: IGlobalWindow
9
+
10
+ ReactDOM.render(
11
+ <Router basename={window.mountPath || "/"}>
12
+ <App />
13
+ </Router>
14
+ , document.getElementById('root') as HTMLElement
15
+ );
@@ -0,0 +1,77 @@
1
+ interface IGlobalWindow {
2
+ mountPath: string
3
+ }
4
+
5
+ interface IWorkerRecord {
6
+ worker_id: string,
7
+ max_thread_size: number,
8
+ current_queue_size: number,
9
+ current_executing_size: number,
10
+ polling_model_names: string[],
11
+ executor_status: string,
12
+ last_heartbeated_at: string,
13
+ }
14
+ interface IWorkersState {
15
+ records: IWorkerRecord[]
16
+ }
17
+ interface IWorkerProps {
18
+ worker: IWorkerRecord
19
+ }
20
+
21
+ interface ISignalRecord {
22
+ worker_id: string,
23
+ signal: string,
24
+ sent_at: string,
25
+ received_at: string,
26
+ }
27
+ interface ISignalsState {
28
+ records: ISignalRecord[]
29
+ }
30
+ interface ISignalProps {
31
+ signal: ISignalRecord
32
+ }
33
+
34
+ interface ISchedulableRecord {
35
+ crono_trigger_status: string,
36
+ id: number,
37
+ cron: string | null,
38
+ next_execute_at: string | null,
39
+ last_executed_at: string | null,
40
+ timezone: string | null,
41
+ execute_lock: number,
42
+ locked_by: string | null,
43
+ started_at: string,
44
+ finished_at: string,
45
+ last_error_name: string,
46
+ last_error_reason: string,
47
+ last_error_time: string,
48
+ retry_count: number,
49
+ time_to_unlock: number,
50
+ delay_sec: number,
51
+ }
52
+ interface ISchedulableRecordsProps {
53
+ model_name: string
54
+ }
55
+ interface ISchedulableRecordsStates {
56
+ records: ISchedulableRecord[],
57
+ timeRangeMinute: number,
58
+ }
59
+
60
+ interface ISchedulableRecordProps {
61
+ model_name: string,
62
+ record: ISchedulableRecord
63
+ }
64
+
65
+ export {
66
+ IGlobalWindow,
67
+ IWorkerRecord,
68
+ IWorkersState,
69
+ IWorkerProps,
70
+ ISignalRecord,
71
+ ISignalsState,
72
+ ISignalProps,
73
+ ISchedulableRecord,
74
+ ISchedulableRecordsProps,
75
+ ISchedulableRecordsStates,
76
+ ISchedulableRecordProps
77
+ }