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