openc3 6.2.1 → 6.4.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -3
  3. data/bin/openc3cli +13 -14
  4. data/data/config/interface_modifiers.yaml +1 -1
  5. data/data/config/microservice.yaml +1 -1
  6. data/data/config/plugins.yaml +1 -1
  7. data/data/config/tool.yaml +1 -1
  8. data/data/config/widgets.yaml +3 -0
  9. data/ext/openc3/ext/burst_protocol/burst_protocol.c +5 -1
  10. data/lib/openc3/api/api.rb +7 -1
  11. data/lib/openc3/api/cmd_api.rb +5 -8
  12. data/lib/openc3/api/interface_api.rb +8 -4
  13. data/lib/openc3/api/tlm_api.rb +23 -8
  14. data/lib/openc3/interfaces/file_interface.rb +36 -7
  15. data/lib/openc3/interfaces/protocols/burst_protocol.rb +3 -3
  16. data/lib/openc3/interfaces/protocols/preidentified_protocol.rb +20 -26
  17. data/lib/openc3/interfaces.rb +4 -3
  18. data/lib/openc3/io/json_api_object.rb +1 -1
  19. data/lib/openc3/io/json_drb_object.rb +1 -1
  20. data/lib/openc3/logs/log_writer.rb +11 -8
  21. data/lib/openc3/logs/packet_log_reader.rb +2 -2
  22. data/lib/openc3/logs/packet_log_writer.rb +1 -1
  23. data/lib/openc3/microservices/interface_decom_common.rb +4 -1
  24. data/lib/openc3/microservices/interface_microservice.rb +69 -6
  25. data/lib/openc3/microservices/microservice.rb +3 -1
  26. data/lib/openc3/microservices/multi_microservice.rb +1 -1
  27. data/lib/openc3/migrations/20250402000000_periodic_only_default.rb +24 -0
  28. data/lib/openc3/models/model.rb +6 -2
  29. data/lib/openc3/models/offline_access_model.rb +7 -6
  30. data/lib/openc3/models/scope_model.rb +5 -2
  31. data/lib/openc3/models/script_status_model.rb +242 -0
  32. data/lib/openc3/models/target_model.rb +150 -15
  33. data/lib/openc3/packets/commands.rb +10 -3
  34. data/lib/openc3/script/api_shared.rb +4 -0
  35. data/lib/openc3/script/commands.rb +1 -1
  36. data/lib/openc3/script/script.rb +14 -0
  37. data/lib/openc3/script/script_runner.rb +22 -7
  38. data/lib/openc3/utilities/authentication.rb +6 -6
  39. data/lib/openc3/utilities/cosmos_rails_formatter.rb +1 -1
  40. data/lib/openc3/utilities/local_mode.rb +5 -2
  41. data/lib/openc3/utilities/message_log.rb +2 -0
  42. data/lib/openc3/utilities/metric.rb +1 -1
  43. data/lib/openc3/utilities/ruby_lex_utils.rb +114 -279
  44. data/lib/openc3/utilities/target_file.rb +6 -2
  45. data/lib/openc3/version.rb +6 -6
  46. data/templates/tool_angular/package.json +2 -2
  47. data/templates/tool_react/package.json +1 -1
  48. data/templates/tool_svelte/package.json +1 -1
  49. data/templates/tool_vue/eslint.config.mjs +17 -41
  50. data/templates/tool_vue/package.json +3 -3
  51. data/templates/widget/package.json +2 -2
  52. metadata +21 -23
  53. data/ext/mkrf_conf.rb +0 -52
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2024, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -309,7 +309,7 @@ module OpenC3
309
309
  if cbor
310
310
  extra = CBOR.decode(extra_encoded)
311
311
  else
312
- extra = JSON.parse(extra_encode, allow_nan: true, create_additions: true)
312
+ extra = JSON.parse(extra_encoded, allow_nan: true, create_additions: true)
313
313
  end
314
314
  end
315
315
  data = entry[next_offset..-1]
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -15,6 +15,9 @@
15
15
  #
16
16
  # This file may also be used under the terms of a commercial license
17
17
  # if purchased from OpenC3, Inc.
18
+ #
19
+ # A portion of this file was funded by Blue Origin Enterprises, L.P.
20
+ # See https://github.com/OpenC3/cosmos/pull/1963
18
21
 
19
22
  require 'openc3/topics/telemetry_topic'
20
23
  require 'openc3/system/system'
@@ -33,8 +36,8 @@ module OpenC3
33
36
  packet.write(name.to_s, value, type)
34
37
  end
35
38
  end
36
- packet.received_count += 1
37
39
  packet.received_time = Time.now.sys
40
+ packet.received_count = TargetModel.increment_telemetry_count(packet.target_name, packet.packet_name, 1, scope: @scope)
38
41
  TelemetryTopic.write_packet(packet, scope: @scope)
39
42
  # If the inject_tlm parameters are bad we rescue so
40
43
  # interface_microservice and decom_microservice can continue
@@ -19,6 +19,9 @@
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
21
21
  # if purchased from OpenC3, Inc.
22
+ #
23
+ # A portion of this file was funded by Blue Origin Enterprises, L.P.
24
+ # See https://github.com/OpenC3/cosmos/pull/1963
22
25
 
23
26
  require 'openc3/microservices/microservice'
24
27
  require 'openc3/microservices/interface_decom_common'
@@ -37,7 +40,7 @@ require 'openc3/interfaces/interface'
37
40
  begin
38
41
  require 'openc3-enterprise/models/critical_cmd_model'
39
42
  rescue LoadError
40
- # Should never actual be used in Open Source
43
+ # LoadError expected in COSMOS Core
41
44
  end
42
45
 
43
46
  module OpenC3
@@ -128,7 +131,7 @@ module OpenC3
128
131
  @logger.info "#{@interface.name}: Write raw"
129
132
  # A raw interface write results in an UNKNOWN packet
130
133
  command = System.commands.packet('UNKNOWN', 'UNKNOWN')
131
- command.received_count += 1
134
+ command.received_count = TargetModel.increment_command_count('UNKNOWN', 'UNKNOWN', 1, scope: @scope)
132
135
  command = command.clone
133
136
  command.buffer = msg_hash['raw']
134
137
  command.received_time = Time.now
@@ -215,14 +218,15 @@ module OpenC3
215
218
  command = System.commands.identify(cmd_buffer, @interface.cmd_target_names)
216
219
  end
217
220
  unless command
218
- command = System.commands.packet('UNKNOWN', 'UNKNOWN')
219
- command.received_count += 1
220
- command = command.clone
221
+ command = System.commands.packet('UNKNOWN', 'UNKNOWN').clone
221
222
  command.buffer = cmd_buffer
222
223
  end
223
224
  else
224
225
  raise "Invalid command received:\n #{msg_hash}"
225
226
  end
227
+ orig_command = System.commands.packet(command.target_name, command.packet_name)
228
+ orig_command.received_count = TargetModel.increment_command_count(command.target_name, command.packet_name, 1, scope: @scope)
229
+ command.received_count = orig_command.received_count
226
230
  command.received_time = Time.now
227
231
  rescue => e
228
232
  @logger.error "#{@interface.name}: #{msg_hash}"
@@ -504,6 +508,9 @@ module OpenC3
504
508
  end
505
509
 
506
510
  @queued = false
511
+ @sync_packet_count_data = {}
512
+ @sync_packet_count_time = nil
513
+ @sync_packet_count_delay_seconds = 1.0 # Sync packet counts every second
507
514
  @interface.options.each do |option_name, option_values|
508
515
  if option_name.upcase == 'OPTIMIZE_THROUGHPUT'
509
516
  @queued = true
@@ -511,6 +518,9 @@ module OpenC3
511
518
  EphemeralStoreQueued.instance.set_update_interval(update_interval)
512
519
  StoreQueued.instance.set_update_interval(update_interval)
513
520
  end
521
+ if option_name.upcase == 'SYNC_PACKET_COUNT_DELAY_SECONDS'
522
+ @sync_packet_count_delay_seconds = option_values[0].to_f
523
+ end
514
524
  end
515
525
 
516
526
  @interface_thread_sleeper = Sleeper.new
@@ -678,7 +688,7 @@ module OpenC3
678
688
  end
679
689
 
680
690
  # Write to stream
681
- packet.received_count += 1
691
+ sync_tlm_packet_counts(packet)
682
692
  TelemetryTopic.write_packet(packet, queued: @queued, scope: @scope)
683
693
  end
684
694
 
@@ -812,6 +822,59 @@ module OpenC3
812
822
  def graceful_kill
813
823
  # Just to avoid warning
814
824
  end
825
+
826
+ def sync_tlm_packet_counts(packet)
827
+ if @sync_packet_count_delay_seconds <= 0
828
+ # Perfect but slow method
829
+ packet.received_count = TargetModel.increment_telemetry_count(packet.target_name, packet.packet_name, 1, scope: @scope)
830
+ else
831
+ # Eventually consistent method
832
+ # Only sync every period (default 1 second) to avoid hammering Redis
833
+ # This is a trade off between speed and accuracy
834
+ # The packet count is eventually consistent
835
+ @sync_packet_count_data[packet.target_name] ||= {}
836
+ @sync_packet_count_data[packet.target_name][packet.packet_name] ||= 0
837
+ @sync_packet_count_data[packet.target_name][packet.packet_name] += 1
838
+
839
+ # Ensures counters change between syncs
840
+ packet.received_count += 1
841
+
842
+ # Check if we need to sync the packet counts
843
+ if @sync_packet_count_time.nil? or (Time.now - @sync_packet_count_time) > @sync_packet_count_delay_seconds
844
+ @sync_packet_count_time = Time.now
845
+
846
+ inc_count = 0
847
+ # Use pipeline to make this one transaction
848
+ result = Store.redis_pool.pipelined do
849
+ # Increment global counters for packets received
850
+ @sync_packet_count_data.each do |target_name, packet_data|
851
+ packet_data.each do |packet_name, count|
852
+ TargetModel.increment_telemetry_count(target_name, packet_name, count, scope: @scope)
853
+ inc_count += 1
854
+ end
855
+ end
856
+ @sync_packet_count_data = {}
857
+
858
+ # Get all the packet counts with the global counters
859
+ @interface.tlm_target_names.each do |target_name|
860
+ TargetModel.get_all_telemetry_counts(target_name, scope: @scope)
861
+ end
862
+ TargetModel.get_all_telemetry_counts('UNKNOWN', scope: @scope)
863
+ end
864
+ @interface.tlm_target_names.each do |target_name|
865
+ result[inc_count].each do |packet_name, count|
866
+ update_packet = System.telemetry.packet(target_name, packet_name)
867
+ update_packet.received_count = count.to_i
868
+ end
869
+ inc_count += 1
870
+ end
871
+ result[inc_count].each do |packet_name, count|
872
+ update_packet = System.telemetry.packet('UNKNOWN', packet_name)
873
+ update_packet.received_count = count.to_i
874
+ end
875
+ end
876
+ end
877
+ end
815
878
  end
816
879
  end
817
880
 
@@ -64,6 +64,7 @@ module OpenC3
64
64
  microservice.state = 'DIED_ERROR'
65
65
  Logger.fatal("Microservice #{name} dying from exception\n#{e.formatted}")
66
66
  end
67
+ microservice.shutdown # Dying in crash so should try to shutdown
67
68
  ensure
68
69
  MicroserviceStatusModel.set(microservice.as_json(:allow_nan => true), scope: microservice.scope)
69
70
  end
@@ -207,9 +208,10 @@ module OpenC3
207
208
  shutdown()
208
209
  end
209
210
 
210
- def shutdown
211
+ def shutdown(state = 'STOPPED')
211
212
  return if @shutdown_complete
212
213
  @logger.info("Shutting down microservice: #{@name}")
214
+ @state = state
213
215
  @cancel_thread = true
214
216
  @microservice_status_sleeper.cancel if @microservice_status_sleeper
215
217
  MicroserviceStatusModel.set(as_json(:allow_nan => true), scope: @scope)
@@ -25,7 +25,7 @@ module OpenC3
25
25
  def run
26
26
  ARGV.each do |microservice_name|
27
27
  microservice_model = MicroserviceModel.get_model(name: microservice_name, scope: @scope)
28
- if microservice_model.enabled
28
+ if microservice_model and microservice_model.enabled
29
29
  thread = Thread.new do
30
30
  cmd_line = microservice_model.cmd.join(' ')
31
31
  split_cmd_line = cmd_line.split(' ')
@@ -0,0 +1,24 @@
1
+ require 'openc3/utilities/migration'
2
+ require 'openc3/models/scope_model'
3
+ require 'openc3/models/microservice_model'
4
+
5
+ module OpenC3
6
+ class PeriodicOnlyDefault < Migration
7
+ def self.run
8
+ ScopeModel.get_all_models(scope: nil).each do |scope, scope_model|
9
+ next if scope == 'DEFAULT'
10
+ model = MicroserviceModel.get_model(name: "#{scope}__SCOPEMULTI__#{scope}", scope: scope)
11
+ if model
12
+ model.cmd.delete("#{scope}__PERIODIC__#{scope}")
13
+ model.update
14
+ end
15
+ model = MicroserviceModel.get_model(name: "#{scope}__PERIODIC__#{scope}", scope: scope)
16
+ model.destroy if model
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ unless ENV['OPENC3_NO_MIGRATE']
23
+ OpenC3::PeriodicOnlyDefault.run
24
+ end
@@ -139,7 +139,7 @@ module OpenC3
139
139
 
140
140
  # Update the Redis hash at primary_key and set the field "name"
141
141
  # to the JSON generated via calling as_json
142
- def create(update: false, force: false, queued: false)
142
+ def create(update: false, force: false, queued: false, isoformat: false)
143
143
  unless force
144
144
  existing = self.class.store.hget(@primary_key, @name)
145
145
  if existing
@@ -148,7 +148,11 @@ module OpenC3
148
148
  raise "#{@primary_key}:#{@name} doesn't exist at update" if update
149
149
  end
150
150
  end
151
- @updated_at = Time.now.to_nsec_from_epoch
151
+ if isoformat
152
+ @updated_at = Time.now.utc.iso8601
153
+ else
154
+ @updated_at = Time.now.utc.to_nsec_from_epoch
155
+ end
152
156
 
153
157
  if queued
154
158
  write_store = self.class.store_queued
@@ -19,28 +19,29 @@
19
19
  require 'openc3/models/model'
20
20
 
21
21
  module OpenC3
22
+ # Note: This model is locked to the DEFAULT scope
22
23
  class OfflineAccessModel < Model
23
- PRIMARY_KEY = 'openc3__offline_access'
24
+ PRIMARY_KEY = 'DEFAULT__openc3__offline_access'
24
25
 
25
26
  attr_accessor :offline_access_token
26
27
 
27
28
  # NOTE: The following three class methods are used by the ModelController
28
29
  # and are reimplemented to enable various Model class methods to work
29
30
  def self.get(name:, scope:)
30
- super("#{scope}__#{PRIMARY_KEY}", name: name)
31
+ super(PRIMARY_KEY, name: name)
31
32
  end
32
33
 
33
34
  def self.names(scope:)
34
- super("#{scope}__#{PRIMARY_KEY}")
35
+ super(PRIMARY_KEY)
35
36
  end
36
37
 
37
38
  def self.all(scope:)
38
- super("#{scope}__#{PRIMARY_KEY}")
39
+ super(PRIMARY_KEY)
39
40
  end
40
41
  # END NOTE
41
42
 
42
- def initialize(name:, offline_access_token: nil, updated_at: nil, scope:)
43
- super("#{scope}__#{PRIMARY_KEY}", name: name, scope: scope)
43
+ def initialize(name:, offline_access_token: nil, updated_at: nil, scope: 'DEFAULT')
44
+ super(PRIMARY_KEY, name: name, updated_at: updated_at, scope: 'DEFAULT')
44
45
  @offline_access_token = offline_access_token
45
46
  end
46
47
 
@@ -350,8 +350,11 @@ module OpenC3
350
350
  # UNKNOWN PacketLog Microservice
351
351
  deploy_unknown_packetlog_microservice(gem_path, variables, @parent)
352
352
 
353
- # Periodic Microservice
354
- deploy_periodic_microservice(gem_path, variables, @parent)
353
+ # Only DEFAULT scope
354
+ if @scope == 'DEFAULT'
355
+ # Periodic Microservice
356
+ deploy_periodic_microservice(gem_path, variables, @parent)
357
+ end
355
358
 
356
359
  # Scope Cleanup Microservice
357
360
  deploy_scopecleanup_microservice(gem_path, variables, @parent)
@@ -0,0 +1,242 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # This file may also be used under the terms of a commercial license
17
+ # if purchased from OpenC3, Inc.
18
+
19
+ require 'openc3/models/model'
20
+
21
+ module OpenC3
22
+ class ScriptStatusModel < Model
23
+ # Note: ScriptRunner only has permissions for keys that start with running-script
24
+ RUNNING_PRIMARY_KEY = 'running-script'
25
+ COMPLETED_PRIMARY_KEY = 'running-script-completed'
26
+
27
+ def id
28
+ return @name
29
+ end
30
+ attr_reader :state # spawning, init, running, paused, waiting, breakpoint, error, crashed, stopped, completed, completed_errors, killed
31
+ attr_accessor :shard
32
+ attr_accessor :filename
33
+ attr_accessor :current_filename
34
+ attr_accessor :line_no
35
+ attr_accessor :start_line_no
36
+ attr_accessor :end_line_no
37
+ attr_accessor :username
38
+ attr_accessor :user_full_name
39
+ attr_accessor :start_time
40
+ attr_accessor :end_time
41
+ attr_accessor :disconnect
42
+ attr_accessor :environment
43
+ attr_accessor :suite_runner
44
+ attr_accessor :errors
45
+ attr_accessor :pid
46
+ attr_accessor :log
47
+ attr_accessor :report
48
+
49
+ # NOTE: The following three class methods are used by the ModelController
50
+ # and are reimplemented to enable various Model class methods to work
51
+ def self.get(name:, scope:, type: "auto")
52
+ if type == "auto" or type == "running"
53
+ # Check for running first
54
+ running = super("#{RUNNING_PRIMARY_KEY}__#{scope}", name: name)
55
+ return running if running
56
+ end
57
+ return super("#{COMPLETED_PRIMARY_KEY}__#{scope}", name: name)
58
+ end
59
+
60
+ def self.names(scope:, type: "running")
61
+ if type == "running"
62
+ return super("#{RUNNING_PRIMARY_KEY}__#{scope}")
63
+ else
64
+ return super("#{COMPLETED_PRIMARY_KEY}__#{scope}")
65
+ end
66
+ end
67
+
68
+ def self.all(scope:, offset: 0, limit: 10, type: "running")
69
+ if type == "running"
70
+ keys = self.store.zrevrange("#{RUNNING_PRIMARY_KEY}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
71
+ return [] if keys.empty?
72
+ result = self.store.redis_pool.pipelined do
73
+ keys.each do |key|
74
+ self.store.hget("#{RUNNING_PRIMARY_KEY}__#{scope}", key)
75
+ end
76
+ end
77
+ result = result.map do |r|
78
+ if r.nil?
79
+ nil
80
+ else
81
+ JSON.parse(r, :allow_nan => true, :create_additions => true)
82
+ end
83
+ end
84
+ return result
85
+ else
86
+ keys = self.store.zrevrange("#{COMPLETED_PRIMARY_KEY}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
87
+ return [] if keys.empty?
88
+ result = self.store.redis_pool.pipelined do
89
+ keys.each do |key|
90
+ self.store.hget("#{COMPLETED_PRIMARY_KEY}__#{scope}", key)
91
+ end
92
+ end
93
+ result = result.map do |r|
94
+ if r.nil?
95
+ nil
96
+ else
97
+ JSON.parse(r, :allow_nan => true, :create_additions => true)
98
+ end
99
+ end
100
+ return result
101
+ end
102
+ end
103
+
104
+ def self.count(scope:, type: "running")
105
+ if type == "running"
106
+ return self.store.zcount("#{RUNNING_PRIMARY_KEY}__#{scope}__LIST", 0, Float::INFINITY)
107
+ else
108
+ return self.store.zcount("#{COMPLETED_PRIMARY_KEY}__#{scope}__LIST", 0, Float::INFINITY)
109
+ end
110
+ end
111
+
112
+ def initialize(
113
+ name:, # id
114
+ state:, # spawning, init, running, paused, waiting, error, breakpoint, crashed, stopped, completed, completed_errors, killed
115
+ shard: 0, # Future enhancement of script runner shards
116
+ filename:, # The initial filename
117
+ current_filename: nil, # The current filename
118
+ line_no: 0, # The current line number
119
+ start_line_no: 1, # The line number to start the script at
120
+ end_line_no: nil, # The line number to end the script at
121
+ username:, # The username of the person who started the script
122
+ user_full_name:, # The full name of the person who started the script
123
+ start_time:, # The time the script started ISO format
124
+ end_time: nil, # The time the script ended ISO format
125
+ disconnect: false,
126
+ environment: nil,
127
+ suite_runner: nil,
128
+ errors: nil,
129
+ pid: nil,
130
+ log: nil,
131
+ report: nil,
132
+ updated_at: nil,
133
+ scope:
134
+ )
135
+ @state = state
136
+ if is_complete?()
137
+ super("#{COMPLETED_PRIMARY_KEY}__#{scope}", name: name, updated_at: updated_at, plugin: nil, scope: scope)
138
+ else
139
+ super("#{RUNNING_PRIMARY_KEY}__#{scope}", name: name, updated_at: updated_at, plugin: nil, scope: scope)
140
+ end
141
+ @shard = shard.to_i
142
+ @filename = filename
143
+ @current_filename = current_filename
144
+ @line_no = line_no
145
+ @start_line_no = start_line_no
146
+ @end_line_no = end_line_no
147
+ @username = username
148
+ @user_full_name = user_full_name
149
+ @start_time = start_time
150
+ @end_time = end_time
151
+ @disconnect = disconnect
152
+ @environment = environment
153
+ @suite_runner = suite_runner
154
+ @errors = errors
155
+ @pid = pid
156
+ @log = log
157
+ @report = report
158
+ end
159
+
160
+ def is_complete?
161
+ return (@state == 'completed' or @state == 'completed_errors' or @state == 'stopped' or @state == 'crashed' or @state == 'killed')
162
+ end
163
+
164
+ def state=(new_state)
165
+ # If the state is already a flavor of complete, leave it alone (first wins)
166
+ if not is_complete?()
167
+ @state = new_state
168
+ # If setting to complete, check for errors
169
+ # and set the state to complete_errors if they exist
170
+ if @state == 'completed' and @errors
171
+ @state = 'completed_errors'
172
+ end
173
+ end
174
+ end
175
+
176
+ # Update the Redis hash at primary_key and set the field "name"
177
+ # to the JSON generated via calling as_json
178
+ def create(update: false, force: false, queued: false, isoformat: true)
179
+ @updated_at = Time.now.utc.to_nsec_from_epoch
180
+
181
+ if queued
182
+ write_store = self.class.store_queued
183
+ else
184
+ write_store = self.class.store
185
+ end
186
+ write_store.hset(@primary_key, @name, JSON.generate(self.as_json(:allow_nan => true), :allow_nan => true))
187
+
188
+ # Also add to ordered set on create
189
+ write_store.zadd(@primary_key + "__LIST", @name.to_i, @name) if not update
190
+ end
191
+
192
+ def update(force: false, queued: false)
193
+ # Magically handle the change from running to completed
194
+ if is_complete?() and @primary_key == "#{RUNNING_PRIMARY_KEY}__#{@scope}"
195
+ # Destroy the running key
196
+ destroy()
197
+ @destroyed = false
198
+
199
+ # Move to completed
200
+ @primary_key = "#{COMPLETED_PRIMARY_KEY}__#{@scope}"
201
+ create(update: false, force: force, queued: queued, isoformat: true)
202
+ else
203
+ create(update: true, force: force, queued: queued, isoformat: true)
204
+ end
205
+ end
206
+
207
+ # Delete the model from the Store
208
+ def destroy
209
+ @destroyed = true
210
+ undeploy()
211
+ self.class.store.hdel(@primary_key, @name)
212
+ # Also remove from ordered set
213
+ self.class.store.zremrangebyscore(@primary_key + "__LIST", @name.to_i, @name.to_i)
214
+ end
215
+
216
+ def as_json(*a)
217
+ {
218
+ 'name' => @name,
219
+ 'state' => @state,
220
+ 'shard' => @shard,
221
+ 'filename' => @filename,
222
+ 'current_filename' => @current_filename,
223
+ 'line_no' => @line_no,
224
+ 'start_line_no' => @start_line_no,
225
+ 'end_line_no' => @end_line_no,
226
+ 'username' => @username,
227
+ 'user_full_name' => @user_full_name,
228
+ 'start_time' => @start_time,
229
+ 'end_time' => @end_time,
230
+ 'disconnect' => @disconnect,
231
+ 'environment' => @environment,
232
+ 'suite_runner' => @suite_runner,
233
+ 'errors' => @errors,
234
+ 'pid' => @pid,
235
+ 'log' => @log,
236
+ 'report' => @report,
237
+ 'updated_at' => @updated_at,
238
+ 'scope' => @scope
239
+ }
240
+ end
241
+ end
242
+ end