mxrb 0.1.2 → 0.1.4
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/bin/mxrb +458 -10
- data/docs/de-DE/README.md +1 -0
- data/docs/de-DE/oql-sql.md +42 -0
- data/docs/de-DE/project-structure.md +201 -2
- data/docs/de-DE/runtime-ruby.md +104 -0
- data/docs/de-DE/runtime-schema-migrations.md +44 -0
- data/docs/de-DE/validation-matrix.md +42 -4
- data/docs/en-US/README.md +1 -0
- data/docs/en-US/oql-sql.md +41 -0
- data/docs/en-US/project-structure.md +192 -2
- data/docs/en-US/runtime-ruby.md +100 -0
- data/docs/en-US/runtime-schema-migrations.md +41 -0
- data/docs/en-US/validation-matrix.md +39 -4
- data/docs/pt-BR/README.md +1 -0
- data/docs/pt-BR/entity-dsl.md +8 -0
- data/docs/pt-BR/oql-sql.md +41 -0
- data/docs/pt-BR/project-structure.md +197 -2
- data/docs/pt-BR/runtime-ruby.md +108 -0
- data/docs/pt-BR/runtime-schema-migrations.md +40 -0
- data/docs/pt-BR/validation-matrix.md +42 -5
- data/docs/pt-BR/writing.md +28 -0
- data/lib/mxrb/cli/help.rb +426 -0
- data/lib/mxrb/cli/release.rb +137 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
- data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
- data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
- data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
- data/lib/mxrb/compiler/packager.rb +27 -13
- data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
- data/lib/mxrb/compiler/portable_packager.rb +16 -11
- data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
- data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
- data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
- data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
- data/lib/mxrb/domain_diagram.rb +409 -0
- data/lib/mxrb/dsl/builder.rb +62 -11
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +265 -45
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +32 -7
- data/lib/mxrb/io/bson_codec.rb +42 -1
- data/lib/mxrb/io/mpr_file.rb +146 -3
- data/lib/mxrb/model/attribute.rb +3 -0
- data/lib/mxrb/model/entity.rb +44 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/module.rb +71 -0
- data/lib/mxrb/model/page.rb +126 -15
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
- data/lib/mxrb/official_marketplace.rb +9 -3
- data/lib/mxrb/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +32 -4
- data/lib/mxrb/progress.rb +242 -0
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +1749 -0
- data/lib/mxrb/ruby_app/preset.rb +506 -0
- data/lib/mxrb/ruby_app/session_manager.rb +122 -0
- data/lib/mxrb/ruby_app.rb +1361 -0
- data/lib/mxrb/runtime/access_control.rb +408 -0
- data/lib/mxrb/runtime/native.rb +1015 -93
- data/lib/mxrb/runtime/scheduler.rb +396 -0
- data/lib/mxrb/runtime/schema_migrator.rb +598 -0
- data/lib/mxrb/runtime/shared_store.rb +335 -0
- data/lib/mxrb/runtime/sqlite_store.rb +729 -0
- data/lib/mxrb/scaffold/generator.rb +11 -3
- data/lib/mxrb/team_server.rb +10 -7
- data/lib/mxrb/uml/activity_diagram.rb +118 -0
- data/lib/mxrb/uml/class_diagram.rb +118 -0
- data/lib/mxrb/uml/sequence_diagram.rb +112 -0
- data/lib/mxrb/uml/server.rb +146 -0
- data/lib/mxrb/uml/support.rb +35 -0
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
- data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
- data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
- data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
- data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
- data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
- data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
- data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
- data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
- data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
- data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
- data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
- data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
- data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
- data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
- data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
- data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
- data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
- data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
- data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
- data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
- data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
- data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
- data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
- data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
- data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
- data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
- data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
- data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
- data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
- data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
- data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
- data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
- data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
- data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
- data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
- data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
- data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
- data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
- data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
- data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
- data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
- data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
- data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
- data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
- data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
- data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
- data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
- data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
- data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
- data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
- data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
- data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
- data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
- data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
- data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
- data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
- data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
- data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
- data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
- data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
- data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
- data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
- data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
- data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
- data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
- data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
- data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
- data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
- data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
- data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
- data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
- data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
- data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
- data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
- data/lib/mxrb/web_ui/domain.html +15 -0
- data/lib/mxrb/web_ui/modeler.html +16 -0
- data/lib/mxrb/web_ui/uml.html +31 -0
- data/lib/mxrb/web_ui.rb +89 -0
- data/lib/mxrb/widget_certification.rb +208 -0
- data/lib/mxrb/writer.rb +278 -41
- data/lib/mxrb.rb +38 -8
- metadata +157 -5
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require_relative 'shared_store'
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require 'tzinfo'
|
|
9
|
+
rescue LoadError
|
|
10
|
+
# UTC, local time and numeric offsets remain available without tzinfo. IANA
|
|
11
|
+
# names fail explicitly when resolved instead of silently becoming UTC.
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Schedule normalization and lifecycle state deliberately live together so a
|
|
15
|
+
# Scheduler instance owns all of its threads and overlap bookkeeping.
|
|
16
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
17
|
+
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
18
|
+
|
|
19
|
+
module Mxrb
|
|
20
|
+
module Runtime
|
|
21
|
+
ScheduledJob = Data.define(
|
|
22
|
+
:name, :qualified_name, :microflow, :schedule, :overlap, :enabled,
|
|
23
|
+
:start_at, :time_zone, :source
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Small stdlib-only scheduled-event runner. Execution, time, and sleeping
|
|
27
|
+
# are injectable so applications can connect it to their Ruby microflow
|
|
28
|
+
# runtime and test it without waiting for wall-clock time.
|
|
29
|
+
class Scheduler
|
|
30
|
+
attr_reader :jobs, :errors
|
|
31
|
+
|
|
32
|
+
def initialize(project, executor: nil, clock: -> { Time.now.utc },
|
|
33
|
+
sleeper: Kernel.method(:sleep), poll_interval: 1.0,
|
|
34
|
+
skip_overlap: true, async: true, on_error: nil, logger: nil,
|
|
35
|
+
coordinator: nil, lease_ttl: 300, owner: nil)
|
|
36
|
+
raise ArgumentError, 'poll_interval must be positive' unless poll_interval.to_f.positive?
|
|
37
|
+
raise ArgumentError, 'lease_ttl must be positive' unless lease_ttl.to_f.positive?
|
|
38
|
+
|
|
39
|
+
@project = project
|
|
40
|
+
@executor = executor || default_executor
|
|
41
|
+
@clock = clock
|
|
42
|
+
@sleeper = sleeper
|
|
43
|
+
@poll_interval = poll_interval.to_f
|
|
44
|
+
@skip_overlap = skip_overlap
|
|
45
|
+
@async = async
|
|
46
|
+
@on_error = on_error
|
|
47
|
+
@logger = logger
|
|
48
|
+
@coordinator = coordinator || MemorySharedStore.new
|
|
49
|
+
@lease_ttl = lease_ttl.to_f
|
|
50
|
+
@owner = owner || "#{Process.pid}-#{SecureRandom.uuid}"
|
|
51
|
+
@time_zones = {}
|
|
52
|
+
@jobs = load_jobs.freeze
|
|
53
|
+
@errors = []
|
|
54
|
+
@last_slots = {}
|
|
55
|
+
@running_jobs = {}
|
|
56
|
+
@workers = []
|
|
57
|
+
@mutex = Mutex.new
|
|
58
|
+
@stopping = false
|
|
59
|
+
@thread = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def running? = @mutex.synchronize { @thread&.alive? == true }
|
|
63
|
+
|
|
64
|
+
def start
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
return self if @thread&.alive?
|
|
67
|
+
|
|
68
|
+
@stopping = false
|
|
69
|
+
@thread = Thread.new { run_loop }
|
|
70
|
+
@thread.name = 'mxrb-scheduler' if @thread.respond_to?(:name=)
|
|
71
|
+
end
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def shutdown(wait: true)
|
|
76
|
+
thread = @mutex.synchronize do
|
|
77
|
+
@stopping = true
|
|
78
|
+
@thread
|
|
79
|
+
end
|
|
80
|
+
thread&.join if wait && thread != Thread.current
|
|
81
|
+
wait_for_jobs if wait
|
|
82
|
+
@mutex.synchronize { @thread = nil unless @thread&.alive? }
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
alias stop shutdown
|
|
86
|
+
|
|
87
|
+
# Evaluates all jobs once and returns the jobs that were dispatched.
|
|
88
|
+
def tick(now = @clock.call, async: @async)
|
|
89
|
+
cleanup_workers
|
|
90
|
+
jobs.filter_map do |job|
|
|
91
|
+
slot = due_slot(job, now)
|
|
92
|
+
next unless slot
|
|
93
|
+
next unless reserve(job, slot, now)
|
|
94
|
+
|
|
95
|
+
dispatch(job, slot, async:)
|
|
96
|
+
job
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def wait_for_jobs
|
|
101
|
+
loop do
|
|
102
|
+
workers = @mutex.synchronize { @workers.select(&:alive?) }
|
|
103
|
+
break if workers.empty?
|
|
104
|
+
|
|
105
|
+
workers.each { _1.join unless _1 == Thread.current }
|
|
106
|
+
end
|
|
107
|
+
cleanup_workers
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def default_executor
|
|
114
|
+
return @project.method(:execute_microflow) if @project.respond_to?(:execute_microflow)
|
|
115
|
+
|
|
116
|
+
raise ArgumentError, 'executor is required unless project responds to execute_microflow'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def load_jobs
|
|
120
|
+
@project.modules.flat_map do |mod|
|
|
121
|
+
Array(mod.scheduled_events).map { normalize_job(mod.name, _1) }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def normalize_job(module_name, source)
|
|
126
|
+
name = fetch(source, 'Name', :name).to_s
|
|
127
|
+
qualified = qualify(module_name, fetch(source, 'QualifiedName', :qualified_name) || name)
|
|
128
|
+
microflow = qualify(module_name, fetch(source, 'Microflow', :microflow))
|
|
129
|
+
schedule = normalize_schedule(source)
|
|
130
|
+
ScheduledJob.new(
|
|
131
|
+
name:, qualified_name: qualified, microflow:, schedule:,
|
|
132
|
+
overlap: fetch(source, 'OnOverlap', :on_overlap, :overlap) || 'SkipNext',
|
|
133
|
+
enabled: fetch(source, 'Enabled', :enabled) != false,
|
|
134
|
+
start_at: parse_time(fetch(source, 'StartDateTime', :start_at)),
|
|
135
|
+
time_zone: fetch(source, 'TimeZone', :time_zone).to_s,
|
|
136
|
+
source:
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def normalize_schedule(source)
|
|
141
|
+
schedule = fetch(source, 'Schedule', :schedule)
|
|
142
|
+
type = fetch(schedule, '$Type', :type).to_s
|
|
143
|
+
case type
|
|
144
|
+
when /MinuteSchedule\z/
|
|
145
|
+
{ type: :minute, every: positive(fetch(schedule, 'Multiplier', :multiplier)) }.freeze
|
|
146
|
+
when /HourSchedule\z/
|
|
147
|
+
{
|
|
148
|
+
type: :hour, every: positive(fetch(schedule, 'Multiplier', :multiplier)),
|
|
149
|
+
minute: integer(fetch(schedule, 'MinuteOffset', :minute_offset), 0) % 60
|
|
150
|
+
}.freeze
|
|
151
|
+
when /DaySchedule\z/
|
|
152
|
+
{
|
|
153
|
+
type: :day, every: 1,
|
|
154
|
+
hour: integer(fetch(schedule, 'HourOfDay', :hour_of_day), 0) % 24,
|
|
155
|
+
minute: integer(fetch(schedule, 'MinuteOfHour', :minute_of_hour), 0) % 60
|
|
156
|
+
}.freeze
|
|
157
|
+
else
|
|
158
|
+
legacy_schedule(source)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def legacy_schedule(source)
|
|
163
|
+
type = fetch(source, 'IntervalType', :interval_type).to_s.downcase
|
|
164
|
+
every = positive(fetch(source, 'Interval', :interval))
|
|
165
|
+
case type
|
|
166
|
+
when 'minute' then { type: :minute, every: }.freeze
|
|
167
|
+
when 'hour' then { type: :hour, every:, minute: 0 }.freeze
|
|
168
|
+
when 'day' then { type: :day, every:, hour: 0, minute: 0 }.freeze
|
|
169
|
+
else
|
|
170
|
+
raise ArgumentError, "unsupported scheduled-event interval #{type.inspect}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def due_slot(job, now)
|
|
175
|
+
return unless job.enabled
|
|
176
|
+
|
|
177
|
+
time = event_time(now, job.time_zone)
|
|
178
|
+
start = job.start_at && event_time(job.start_at, job.time_zone)
|
|
179
|
+
return if start && time < start
|
|
180
|
+
|
|
181
|
+
schedule = job.schedule
|
|
182
|
+
case schedule.fetch(:type)
|
|
183
|
+
when :minute
|
|
184
|
+
minute = time.to_i / 60
|
|
185
|
+
anchor = start ? start.to_i / 60 : 0
|
|
186
|
+
minute if ((minute - anchor) % schedule.fetch(:every)).zero?
|
|
187
|
+
when :hour
|
|
188
|
+
return unless time.min == schedule.fetch(:minute)
|
|
189
|
+
|
|
190
|
+
hour = time.to_i / 3600
|
|
191
|
+
anchor = start ? start.to_i / 3600 : 0
|
|
192
|
+
hour if ((hour - anchor) % schedule.fetch(:every)).zero?
|
|
193
|
+
when :day
|
|
194
|
+
return unless time.hour == schedule.fetch(:hour) && time.min == schedule.fetch(:minute)
|
|
195
|
+
|
|
196
|
+
day = time.to_date.jd
|
|
197
|
+
anchor = start ? start.to_date.jd : 0
|
|
198
|
+
day if ((day - anchor) % schedule.fetch(:every)).zero?
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def reserve(job, slot, now)
|
|
203
|
+
@mutex.synchronize do
|
|
204
|
+
return false if @last_slots[job.qualified_name] == slot
|
|
205
|
+
return false if skip_overlap?(job) && @running_jobs[job.qualified_name]
|
|
206
|
+
|
|
207
|
+
slot_key = "#{job.schedule.fetch(:type)}:#{slot}"
|
|
208
|
+
claimed = @coordinator.claim_scheduled_event(
|
|
209
|
+
event: job.qualified_name, slot: slot_key, owner: @owner,
|
|
210
|
+
now:, lease_until: now + @lease_ttl, skip_overlap: skip_overlap?(job)
|
|
211
|
+
)
|
|
212
|
+
return false unless claimed
|
|
213
|
+
|
|
214
|
+
@last_slots[job.qualified_name] = slot
|
|
215
|
+
@running_jobs[job.qualified_name] = slot_key
|
|
216
|
+
true
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def skip_overlap?(job)
|
|
221
|
+
@skip_overlap || job.overlap.to_s.match?(/skip/i)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def dispatch(job, slot = nil, async:)
|
|
225
|
+
slot_key = slot && "#{job.schedule.fetch(:type)}:#{slot}"
|
|
226
|
+
slot_key ||= @running_jobs[job.qualified_name] || "manual:#{SecureRandom.uuid}"
|
|
227
|
+
unless async
|
|
228
|
+
execute(job, slot_key)
|
|
229
|
+
return
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
worker = Thread.new { execute(job, slot_key) }
|
|
233
|
+
worker.name = "mxrb-job-#{job.qualified_name}" if worker.respond_to?(:name=)
|
|
234
|
+
@mutex.synchronize { @workers << worker }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def execute(job, slot)
|
|
238
|
+
heartbeat = start_heartbeat(job, slot)
|
|
239
|
+
invoke_executor(job)
|
|
240
|
+
rescue StandardError => e
|
|
241
|
+
@mutex.synchronize { @errors << [job, e].freeze }
|
|
242
|
+
@on_error&.call(job, e)
|
|
243
|
+
@logger&.error("scheduled event #{job.qualified_name} failed: #{e.message}")
|
|
244
|
+
ensure
|
|
245
|
+
stop_heartbeat(heartbeat)
|
|
246
|
+
begin
|
|
247
|
+
@coordinator.complete_scheduled_event(
|
|
248
|
+
event: job.qualified_name, slot:, owner: @owner
|
|
249
|
+
)
|
|
250
|
+
rescue StandardError => e
|
|
251
|
+
@logger&.error("scheduled event #{job.qualified_name} lease completion failed: #{e.message}")
|
|
252
|
+
ensure
|
|
253
|
+
@mutex.synchronize { @running_jobs.delete(job.qualified_name) }
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def start_heartbeat(job, slot)
|
|
258
|
+
return unless @coordinator.respond_to?(:renew_scheduled_event)
|
|
259
|
+
|
|
260
|
+
state = { mutex: Mutex.new, condition: ConditionVariable.new, stopping: false }
|
|
261
|
+
state[:thread] = Thread.start do
|
|
262
|
+
heartbeat_loop(job, slot, state)
|
|
263
|
+
end
|
|
264
|
+
state[:thread].name = "mxrb-lease-#{job.qualified_name}"
|
|
265
|
+
state
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def heartbeat_loop(job, slot, state)
|
|
269
|
+
interval = [@lease_ttl / 3.0, 0.01].max
|
|
270
|
+
loop do
|
|
271
|
+
stopping = state.fetch(:mutex).synchronize do
|
|
272
|
+
if state.fetch(:stopping)
|
|
273
|
+
true
|
|
274
|
+
else
|
|
275
|
+
state.fetch(:condition).wait(state.fetch(:mutex), interval)
|
|
276
|
+
state.fetch(:stopping)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
break if stopping
|
|
280
|
+
|
|
281
|
+
now = @clock.call
|
|
282
|
+
renewed = @coordinator.renew_scheduled_event(
|
|
283
|
+
event: job.qualified_name, slot:, owner: @owner,
|
|
284
|
+
lease_until: now + @lease_ttl
|
|
285
|
+
)
|
|
286
|
+
break unless renewed
|
|
287
|
+
end
|
|
288
|
+
rescue StandardError => e
|
|
289
|
+
@logger&.error("scheduled event #{job.qualified_name} lease renewal failed: #{e.message}")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def stop_heartbeat(state)
|
|
293
|
+
return unless state
|
|
294
|
+
|
|
295
|
+
state.fetch(:mutex).synchronize do
|
|
296
|
+
state[:stopping] = true
|
|
297
|
+
state.fetch(:condition).broadcast
|
|
298
|
+
end
|
|
299
|
+
state.fetch(:thread).join
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def invoke_executor(job)
|
|
303
|
+
callable = if @executor.respond_to?(:execute_microflow)
|
|
304
|
+
@executor.method(:execute_microflow)
|
|
305
|
+
elsif @executor.respond_to?(:call)
|
|
306
|
+
@executor
|
|
307
|
+
else
|
|
308
|
+
raise ArgumentError, 'executor must be callable or implement execute_microflow'
|
|
309
|
+
end
|
|
310
|
+
case callable.arity
|
|
311
|
+
when 0 then callable.call
|
|
312
|
+
when 1 then callable.call(job.microflow)
|
|
313
|
+
else callable.call(job.microflow, scheduled_event: job)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def run_loop
|
|
318
|
+
until stopping?
|
|
319
|
+
tick
|
|
320
|
+
@sleeper.call(@poll_interval) unless stopping?
|
|
321
|
+
end
|
|
322
|
+
rescue StandardError => e
|
|
323
|
+
@mutex.synchronize { @errors << [nil, e].freeze }
|
|
324
|
+
@on_error&.call(nil, e)
|
|
325
|
+
@logger&.error("scheduler failed: #{e.message}")
|
|
326
|
+
ensure
|
|
327
|
+
@mutex.synchronize { @thread = nil if @thread == Thread.current }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def stopping? = @mutex.synchronize { @stopping }
|
|
331
|
+
|
|
332
|
+
def cleanup_workers
|
|
333
|
+
@mutex.synchronize do
|
|
334
|
+
@workers.each { _1.join if !_1.alive? && _1 != Thread.current }
|
|
335
|
+
@workers.select!(&:alive?)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def event_time(time, zone)
|
|
340
|
+
return time.getutc if zone.empty? || zone.casecmp('UTC').zero?
|
|
341
|
+
return time.getlocal if zone.casecmp('local').zero?
|
|
342
|
+
return time.getlocal(zone) if zone.match?(/\A[+-]\d{2}:?\d{2}\z/)
|
|
343
|
+
|
|
344
|
+
time_zone(zone).utc_to_local(time.getutc)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def time_zone(name)
|
|
348
|
+
unless defined?(TZInfo::Timezone)
|
|
349
|
+
raise ArgumentError,
|
|
350
|
+
"IANA time zone #{name.inspect} requires the tzinfo gem"
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
@time_zones[name] ||= TZInfo::Timezone.get(name)
|
|
354
|
+
rescue StandardError => e
|
|
355
|
+
raise unless defined?(TZInfo::InvalidTimezoneIdentifier) &&
|
|
356
|
+
e.is_a?(TZInfo::InvalidTimezoneIdentifier)
|
|
357
|
+
|
|
358
|
+
raise ArgumentError, "unknown scheduled-event time zone #{name.inspect}"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def parse_time(raw)
|
|
362
|
+
return raw if raw.respond_to?(:to_time)
|
|
363
|
+
return if raw.nil? || raw.to_s.empty?
|
|
364
|
+
|
|
365
|
+
Time.iso8601(raw.to_s)
|
|
366
|
+
rescue ArgumentError
|
|
367
|
+
nil
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def qualify(module_name, raw)
|
|
371
|
+
name = raw.to_s
|
|
372
|
+
return name if name.empty? || name.include?('.')
|
|
373
|
+
|
|
374
|
+
"#{module_name}.#{name}"
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def fetch(hash, *keys)
|
|
378
|
+
return nil unless hash.respond_to?(:key?)
|
|
379
|
+
|
|
380
|
+
keys.each { return hash[_1] if hash.key?(_1) }
|
|
381
|
+
nil
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def positive(value)
|
|
385
|
+
number = integer(value, 1)
|
|
386
|
+
raise ArgumentError, 'scheduled-event interval must be positive' unless number.positive?
|
|
387
|
+
|
|
388
|
+
number
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def integer(value, fallback) = value.nil? ? fallback : Integer(value)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
396
|
+
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|