openc3-cosmos-cfdp 2.4.0 → 2.4.1
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 +2 -2
- data/microservices/CFDP/app/models/cfdp_mib.rb +5 -3
- data/microservices/CFDP/app/models/cfdp_receive_transaction.rb +8 -0
- data/microservices/CFDP/app/models/cfdp_source_transaction.rb +5 -0
- data/microservices/CFDP/app/models/cfdp_transaction.rb +15 -1
- data/microservices/CFDP/app/models/cfdp_user.rb +26 -6
- data/microservices/CFDP/spec/models/cfdp_mib_spec.rb +29 -0
- data/microservices/CFDP/spec/models/cfdp_receive_transaction_spec.rb +8 -0
- data/microservices/CFDP/spec/models/cfdp_source_transaction_spec.rb +25 -0
- data/microservices/CFDP/spec/models/cfdp_transaction_spec.rb +42 -1
- data/microservices/CFDP/spec/models/cfdp_user_spec.rb +91 -1
- data/plugin.txt +1 -1
- data/pyproject.toml +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a885fc317d2f4edaa156c1f3a44adce0a75f3ebc2673909f94d404011876361
|
|
4
|
+
data.tar.gz: 7824550d0da234cdb343886ad2d1e69e338d69b65b8cc31010e025578abf799b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdbeb2b8e886d63646dba3854ce22b3ac042c16eec0580620570f9ffa04b88e78ad23c71e777183cd5852de71bafd6942bd58fbf5e060758454b6c266c43791e
|
|
7
|
+
data.tar.gz: b40bb71ccdc4e024a70702349e1b1e669d66eb2ff9ad5325381aa4605027df25d9927c93a8a814bd1fc8eca3164b844425c0bc23630f2366e406e60a482e8f69
|
data/README.md
CHANGED
|
@@ -149,7 +149,7 @@ These settings are applied to the CFDP microservice via `OPTION <name> <value>`
|
|
|
149
149
|
| bucket | The bucket to send/receive files from. Must be the actual S3 bucket name when using AWS S3 directly (see [Using AWS S3](#using-aws-s3-or-another-s3-service-directly)) | Valid bucket Name | nil - Serve from mounted volume |
|
|
150
150
|
| prevent_received_file_overwrite | Appends a timestamp to the file name for received files if the file already exists | true or false | true |
|
|
151
151
|
| allow_duplicate_transaction_ids | Allows receiving transactions with an ID that was previously used by a completed transaction by deleting the old transaction. Does not affect metadata received for a transaction that is still running, which is never treated as a conflict. | true or false | false |
|
|
152
|
-
| transaction_cleanup_frequency_hours | How often to purge
|
|
152
|
+
| transaction_cleanup_frequency_hours | How often to check for and purge completed transactions from RAM and Redis. A check also runs at startup. How long a completed transaction is kept is set by the local entity's `transaction_retain_seconds`. | Any integer | 24 |
|
|
153
153
|
|
|
154
154
|
> Note: `plugin_test_mode` is a plugin.txt build `VARIABLE` (not a MIB `OPTION`). When set true it installs two dummy entities/targets that can send/receive transactions to each other for testing. It is consumed when the plugin is built, not by the CFDP microservice.
|
|
155
155
|
|
|
@@ -171,7 +171,7 @@ These settings are applied via `OPTION <name> <value>` after declaring the `sour
|
|
|
171
171
|
| enable_eof_nak | Send a NAK in response to a received EOF. Only the local (receiving) entity's value is used. | true or false | false |
|
|
172
172
|
| transaction_closure_requested | Default closure requested setting used when this engine sends a file. Read from the local entity. | CLOSURE_REQUESTED or CLOSURE_NOT_REQUESTED | CLOSURE_REQUESTED |
|
|
173
173
|
| fault_handler | Fault handler setting. Only the local (source) entity's fault handlers are consulted. | (ACK_LIMIT_REACHED, KEEP_ALIVE_LIMIT_REACHED, INVALID_TRANSMISSION_MODE, FILESTORE_REJECTION, FILE_CHECKSUM_FAILURE, FILE_SIZE_ERROR, NAK_LIMIT_REACHED, INACTIVITY_DETECTED, INVALID_FILE_STRUCTURE, CHECK_LIMIT_REACHED, or UNSUPPORTED_CHECKSUM_TYPE) followed by (ISSUE_NOTICE_OF_CANCELLATION, ISSUE_NOTICE_OF_SUSPENSION, IGNORE_ERROR, or ABANDON_TRANSACTION) | See Code |
|
|
174
|
-
| transaction_retain_seconds | Time to keep completed transactions in seconds. Only the source entity's value is used.
|
|
174
|
+
| transaction_retain_seconds | Time to keep completed (FINISHED, CANCELED or ABANDONED) transactions in seconds before they are purged. Purging happens at startup and every `transaction_cleanup_frequency_hours`, so a transaction can be kept up to that much longer. Cleanup also waits for running source threads and pending Finished acknowledgments. Transactions that never complete are not purged and must be canceled. Only the source entity's value is used. | Floating point value greater than 0 | 86400 |
|
|
175
175
|
|
|
176
176
|
### Remote (Destination) Entity Configuration
|
|
177
177
|
|
|
@@ -765,12 +765,14 @@ class CfdpMib
|
|
|
765
765
|
CfdpTransaction.clear_saved_transaction_ids
|
|
766
766
|
end
|
|
767
767
|
|
|
768
|
-
def self.cleanup_old_transactions
|
|
768
|
+
def self.cleanup_old_transactions(skip_ids: [])
|
|
769
769
|
current_time = Time.now.utc
|
|
770
770
|
transaction_retain_seconds = @@entities[@@source_entity_id]['transaction_retain_seconds']
|
|
771
771
|
cleaned_up_count = 0
|
|
772
|
-
|
|
773
|
-
|
|
772
|
+
# Iterate a copy because Rails request threads may add transactions concurrently
|
|
773
|
+
@@transactions.to_a.each do |id, transaction|
|
|
774
|
+
next if skip_ids.include?(id)
|
|
775
|
+
if transaction.complete_time and (current_time - transaction.complete_time) > transaction_retain_seconds and transaction.cleanup_ready?
|
|
774
776
|
transaction.delete
|
|
775
777
|
cleaned_up_count += 1
|
|
776
778
|
end
|
|
@@ -204,6 +204,12 @@ class CfdpReceiveTransaction < CfdpTransaction
|
|
|
204
204
|
end
|
|
205
205
|
end
|
|
206
206
|
|
|
207
|
+
# Completion is reported before the positive acknowledgment procedure ends. Retain the
|
|
208
|
+
# transaction so its Finished PDU can still be retransmitted, including after a restart.
|
|
209
|
+
def cleanup_ready?
|
|
210
|
+
super && @finished_ack_timeout.nil?
|
|
211
|
+
end
|
|
212
|
+
|
|
207
213
|
def complete_file_received?
|
|
208
214
|
return false unless @file_size
|
|
209
215
|
return true if @file_size == 0
|
|
@@ -318,6 +324,7 @@ class CfdpReceiveTransaction < CfdpTransaction
|
|
|
318
324
|
@condition_code = "ACK_LIMIT_REACHED"
|
|
319
325
|
handle_fault()
|
|
320
326
|
@finished_ack_timeout = nil
|
|
327
|
+
save_state()
|
|
321
328
|
else
|
|
322
329
|
@finished_ack_timeout = Time.now + CfdpMib.entity(@source_entity_id)['ack_timer_interval']
|
|
323
330
|
end
|
|
@@ -637,6 +644,7 @@ class CfdpReceiveTransaction < CfdpTransaction
|
|
|
637
644
|
'metadata_pdu_hash' => @metadata_pdu_hash,
|
|
638
645
|
'metadata_pdu_count' => @metadata_pdu_count,
|
|
639
646
|
'create_time' => @create_time&.iso8601(6),
|
|
647
|
+
'complete_time' => @complete_time&.iso8601(6),
|
|
640
648
|
'proxy_response_info' => @proxy_response_info,
|
|
641
649
|
'proxy_response_needed' => @proxy_response_needed,
|
|
642
650
|
'canceling_entity_id' => @canceling_entity_id,
|
|
@@ -598,6 +598,7 @@ class CfdpSourceTransaction < CfdpTransaction
|
|
|
598
598
|
end
|
|
599
599
|
end
|
|
600
600
|
@proxy_response_needed = true if @proxy_response_info
|
|
601
|
+
save_state()
|
|
601
602
|
end
|
|
602
603
|
|
|
603
604
|
def handle_pdu(pdu_hash)
|
|
@@ -724,6 +725,7 @@ class CfdpSourceTransaction < CfdpTransaction
|
|
|
724
725
|
'metadata_pdu_hash' => @metadata_pdu_hash,
|
|
725
726
|
'metadata_pdu_count' => @metadata_pdu_count,
|
|
726
727
|
'create_time' => @create_time&.iso8601(6),
|
|
728
|
+
'complete_time' => @complete_time&.iso8601(6),
|
|
727
729
|
'proxy_response_info' => @proxy_response_info,
|
|
728
730
|
'proxy_response_needed' => @proxy_response_needed,
|
|
729
731
|
'canceling_entity_id' => @canceling_entity_id,
|
|
@@ -784,6 +786,9 @@ class CfdpSourceTransaction < CfdpTransaction
|
|
|
784
786
|
@eof_ack_pdu_hash = state_data['eof_ack_pdu_hash']
|
|
785
787
|
@keep_alive_pdu_hash = state_data['keep_alive_pdu_hash']
|
|
786
788
|
@copy_state = state_data['copy_state']
|
|
789
|
+
# notice_of_completion did not used to save state, so a finished transfer could be saved
|
|
790
|
+
# with a copy_state of complete while its state was still ACTIVE
|
|
791
|
+
@complete_time ||= legacy_complete_time if @copy_state == "complete"
|
|
787
792
|
@file_offset = state_data['file_offset']
|
|
788
793
|
@file_checksum = state_data['file_checksum']
|
|
789
794
|
@file_size = state_data['file_size']
|
|
@@ -301,6 +301,7 @@ class CfdpTransaction
|
|
|
301
301
|
'metadata_pdu_hash' => @metadata_pdu_hash,
|
|
302
302
|
'metadata_pdu_count' => @metadata_pdu_count,
|
|
303
303
|
'create_time' => @create_time&.iso8601(6),
|
|
304
|
+
'complete_time' => @complete_time&.iso8601(6),
|
|
304
305
|
'proxy_response_info' => @proxy_response_info,
|
|
305
306
|
'proxy_response_needed' => @proxy_response_needed,
|
|
306
307
|
'canceling_entity_id' => @canceling_entity_id,
|
|
@@ -341,7 +342,10 @@ class CfdpTransaction
|
|
|
341
342
|
@metadata_pdu_hash = state_data['metadata_pdu_hash']
|
|
342
343
|
@metadata_pdu_count = state_data['metadata_pdu_count'] || 0
|
|
343
344
|
@create_time = state_data['create_time'] ? Time.parse(state_data['create_time']) : nil
|
|
344
|
-
@complete_time =
|
|
345
|
+
@complete_time = state_data['complete_time'] ? Time.parse(state_data['complete_time']) : nil
|
|
346
|
+
# State saved before complete_time was persisted has no complete_time even when the
|
|
347
|
+
# transaction is done, which kept it from ever being cleaned up
|
|
348
|
+
@complete_time ||= legacy_complete_time if ['FINISHED', 'CANCELED', 'ABANDONED'].include?(@state)
|
|
345
349
|
@proxy_response_info = state_data['proxy_response_info']
|
|
346
350
|
@proxy_response_needed = state_data['proxy_response_needed']
|
|
347
351
|
@canceling_entity_id = state_data['canceling_entity_id']
|
|
@@ -353,6 +357,16 @@ class CfdpTransaction
|
|
|
353
357
|
return state_data
|
|
354
358
|
end
|
|
355
359
|
|
|
360
|
+
def cleanup_ready?
|
|
361
|
+
!@complete_time.nil?
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Best estimate of when a completed transaction loaded without a complete_time finished. The
|
|
365
|
+
# create_time is persisted and always precedes completion.
|
|
366
|
+
def legacy_complete_time
|
|
367
|
+
@create_time || Time.now.utc
|
|
368
|
+
end
|
|
369
|
+
|
|
356
370
|
def cfdp_cmd(entity, target_name, packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
|
|
357
371
|
cmd(target_name, packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
|
|
358
372
|
cmd_delay(entity['cmd_delay']) if entity['cmd_delay']
|
|
@@ -44,7 +44,9 @@ class CfdpUser
|
|
|
44
44
|
# Protected by @source_mutex along with @source_transactions.
|
|
45
45
|
@stopping = false
|
|
46
46
|
@source_mutex = Mutex.new
|
|
47
|
-
|
|
47
|
+
# nil so cleanup runs on the first pass through the receive loop. Otherwise a microservice
|
|
48
|
+
# restarted more often than transaction_cleanup_frequency_hours would never clean up.
|
|
49
|
+
@last_cleanup_time = nil
|
|
48
50
|
|
|
49
51
|
at_exit do
|
|
50
52
|
stop()
|
|
@@ -53,6 +55,9 @@ class CfdpUser
|
|
|
53
55
|
|
|
54
56
|
def start
|
|
55
57
|
@source_mutex.synchronize { @stopping = false }
|
|
58
|
+
# Capture restored transactions before returning to Rails. New Put requests start their
|
|
59
|
+
# own threads and must not also be resumed after telemetry initialization finishes.
|
|
60
|
+
transactions_to_resume = CfdpMib.transactions.to_a
|
|
56
61
|
@thread = Thread.new do
|
|
57
62
|
begin
|
|
58
63
|
source_entity = CfdpMib.source_entity
|
|
@@ -73,6 +78,9 @@ class CfdpUser
|
|
|
73
78
|
else
|
|
74
79
|
OpenC3::Topic.update_topic_offsets(topics)
|
|
75
80
|
end
|
|
81
|
+
# Initialize offsets before resumed transfers can generate replies, and register their
|
|
82
|
+
# threads before the first cleanup pass so active transfers are protected from deletion.
|
|
83
|
+
resume_incomplete_source_transactions(transactions_to_resume)
|
|
76
84
|
while !@cancel_thread
|
|
77
85
|
# TODO: Handle freezing transactions if interface disconnects (or target goes unhealthy), and unfreezing if comes back to functional
|
|
78
86
|
|
|
@@ -86,8 +94,8 @@ class CfdpUser
|
|
|
86
94
|
|
|
87
95
|
current_time = Time.now.utc
|
|
88
96
|
frequency_seconds = CfdpMib.transaction_cleanup_frequency_hours * 3600
|
|
89
|
-
if (current_time - @last_cleanup_time) >= frequency_seconds
|
|
90
|
-
CfdpMib.cleanup_old_transactions()
|
|
97
|
+
if @last_cleanup_time.nil? or (current_time - @last_cleanup_time) >= frequency_seconds
|
|
98
|
+
CfdpMib.cleanup_old_transactions(skip_ids: active_source_transaction_ids())
|
|
91
99
|
@last_cleanup_time = current_time
|
|
92
100
|
end
|
|
93
101
|
end
|
|
@@ -96,7 +104,6 @@ class CfdpUser
|
|
|
96
104
|
end
|
|
97
105
|
end
|
|
98
106
|
|
|
99
|
-
resume_incomplete_source_transactions
|
|
100
107
|
return @thread
|
|
101
108
|
end
|
|
102
109
|
|
|
@@ -179,6 +186,8 @@ class CfdpUser
|
|
|
179
186
|
proxy_responses << params
|
|
180
187
|
transaction.proxy_response_needed = false
|
|
181
188
|
transaction.proxy_response_info = nil
|
|
189
|
+
# Completed transactions are persisted, so the response would be sent again on restart
|
|
190
|
+
transaction.save_state
|
|
182
191
|
end
|
|
183
192
|
rescue => err
|
|
184
193
|
# A single bad transaction must not take down the receive thread, which would leave the
|
|
@@ -199,6 +208,15 @@ class CfdpUser
|
|
|
199
208
|
end
|
|
200
209
|
end
|
|
201
210
|
|
|
211
|
+
# Ids of source transactions whose thread is still running. A canceled transaction already has
|
|
212
|
+
# a complete_time while its thread finishes sending EOF and waiting for the Finished PDU, so it
|
|
213
|
+
# must not be cleaned up until the thread is done with it.
|
|
214
|
+
def active_source_transaction_ids
|
|
215
|
+
@source_mutex.synchronize do
|
|
216
|
+
@source_transactions.filter_map { |transaction, thread| transaction.id if thread.alive? }
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
202
220
|
def receive_packet(topic, msg_id, msg_hash, redis)
|
|
203
221
|
topic_split = topic.gsub(/{|}/, '').split("__") # Remove the redis hashtag curly braces
|
|
204
222
|
target_name = topic_split[2]
|
|
@@ -278,8 +296,10 @@ class CfdpUser
|
|
|
278
296
|
return false
|
|
279
297
|
end
|
|
280
298
|
|
|
281
|
-
|
|
282
|
-
|
|
299
|
+
# transactions is a snapshot of CfdpMib.transactions taken by start before Rails can add new
|
|
300
|
+
# transactions concurrently. Put requests start their own threads and must not be resumed too.
|
|
301
|
+
def resume_incomplete_source_transactions(transactions)
|
|
302
|
+
transactions.each do |transaction_id, transaction|
|
|
283
303
|
if transaction_id.split('__')[0].to_i == CfdpMib.source_entity_id && transaction.copy_state != "complete" # && transaction.copy_state != nil?
|
|
284
304
|
# A transaction that was shut down by a previous stop() in this same process would
|
|
285
305
|
# otherwise exit immediately at its first shutdown check
|
|
@@ -875,6 +875,7 @@ RSpec.describe CfdpMib do
|
|
|
875
875
|
allow(@recent_tx).to receive(:delete)
|
|
876
876
|
|
|
877
877
|
@old_tx = double('Transaction')
|
|
878
|
+
allow(@old_tx).to receive(:cleanup_ready?).and_return(true)
|
|
878
879
|
allow(@old_tx).to receive(:complete_time).and_return(Time.now.utc - 120)
|
|
879
880
|
allow(@old_tx).to receive(:delete)
|
|
880
881
|
|
|
@@ -919,9 +920,37 @@ RSpec.describe CfdpMib do
|
|
|
919
920
|
expect(@old_tx).not_to have_received(:delete)
|
|
920
921
|
end
|
|
921
922
|
|
|
923
|
+
it 'skips transactions in skip_ids even when they are old' do
|
|
924
|
+
# A canceled source transaction has a complete_time while its thread is still running
|
|
925
|
+
CfdpMib.cleanup_old_transactions(skip_ids: ['tx3'])
|
|
926
|
+
expect(@old_tx).not_to have_received(:delete)
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
%w(FINISHED CANCELED).each do |state|
|
|
930
|
+
it "retains a restored #{state} receiver until its Finished ACK arrives" do
|
|
931
|
+
id = '2__123'
|
|
932
|
+
state_data = { 'id' => id, 'state' => state,
|
|
933
|
+
'create_time' => (Time.now.utc - 120).iso8601(6),
|
|
934
|
+
'finished_ack_timeout' => (Time.now.utc + 5).iso8601(6) }
|
|
935
|
+
OpenC3::Store.set("#{CfdpTransaction.redis_key_prefix}cfdp_transaction_state:#{id}", JSON.generate(state_data))
|
|
936
|
+
transaction = CfdpReceiveTransaction.allocate
|
|
937
|
+
transaction.load_state(id)
|
|
938
|
+
CfdpMib.transactions[id] = transaction
|
|
939
|
+
allow(transaction).to receive(:delete)
|
|
940
|
+
|
|
941
|
+
CfdpMib.cleanup_old_transactions
|
|
942
|
+
expect(transaction).not_to have_received(:delete)
|
|
943
|
+
|
|
944
|
+
transaction.handle_pdu({ 'DIRECTIVE_CODE' => 'ACK' })
|
|
945
|
+
CfdpMib.cleanup_old_transactions
|
|
946
|
+
expect(transaction).to have_received(:delete)
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
|
|
922
950
|
it 'handles multiple old transactions' do
|
|
923
951
|
# Add another old transaction
|
|
924
952
|
old_tx2 = double('Transaction')
|
|
953
|
+
allow(old_tx2).to receive(:cleanup_ready?).and_return(true)
|
|
925
954
|
allow(old_tx2).to receive(:complete_time).and_return(Time.now.utc - 150)
|
|
926
955
|
allow(old_tx2).to receive(:delete)
|
|
927
956
|
CfdpMib.transactions['tx4'] = old_tx2
|
|
@@ -549,8 +549,11 @@ RSpec.describe CfdpReceiveTransaction do
|
|
|
549
549
|
|
|
550
550
|
it "handles finished_ack limit reached" do
|
|
551
551
|
receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
|
|
552
|
+
receive_transaction.instance_variable_set(:@state, "FINISHED")
|
|
553
|
+
receive_transaction.instance_variable_set(:@complete_time, Time.now.utc - 120)
|
|
552
554
|
receive_transaction.instance_variable_set(:@finished_ack_timeout, Time.now - 1)
|
|
553
555
|
receive_transaction.instance_variable_set(:@finished_pdu, "FINISHED_PDU")
|
|
556
|
+
receive_transaction.save_state
|
|
554
557
|
expect(receive_transaction).to receive(:handle_fault)
|
|
555
558
|
|
|
556
559
|
# Set count to reach limit
|
|
@@ -559,6 +562,11 @@ RSpec.describe CfdpReceiveTransaction do
|
|
|
559
562
|
|
|
560
563
|
expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("ACK_LIMIT_REACHED")
|
|
561
564
|
expect(receive_transaction.instance_variable_get(:@finished_ack_timeout)).to be_nil
|
|
565
|
+
|
|
566
|
+
restored = CfdpReceiveTransaction.allocate
|
|
567
|
+
restored.load_state(receive_transaction.id)
|
|
568
|
+
expect(restored.condition_code).to eq("ACK_LIMIT_REACHED")
|
|
569
|
+
expect(restored.cleanup_ready?).to be true
|
|
562
570
|
end
|
|
563
571
|
end
|
|
564
572
|
|
|
@@ -97,6 +97,31 @@ RSpec.describe CfdpSourceTransaction do
|
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
describe "complete_time persistence" do
|
|
101
|
+
it "saves complete_time when notice_of_completion runs" do
|
|
102
|
+
mock_redis
|
|
103
|
+
transaction = CfdpSourceTransaction.new
|
|
104
|
+
transaction.notice_of_completion
|
|
105
|
+
|
|
106
|
+
loaded = CfdpSourceTransaction.new
|
|
107
|
+
loaded.load_state(transaction.id)
|
|
108
|
+
expect(loaded.state).to eq("FINISHED")
|
|
109
|
+
expect(loaded.complete_time.to_f).to be_within(0.001).of(transaction.complete_time.to_f)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "uses create_time as complete_time for a completed copy saved without one" do
|
|
113
|
+
# notice_of_completion used to not save state, leaving the transaction saved as ACTIVE
|
|
114
|
+
mock_redis
|
|
115
|
+
state_data = {"id" => "1__123", "state" => "ACTIVE", "copy_state" => "complete",
|
|
116
|
+
"create_time" => "2023-01-01T10:00:00.000000Z"}
|
|
117
|
+
OpenC3::Store.set("#{CfdpTransaction.redis_key_prefix}cfdp_transaction_state:1__123", JSON.generate(state_data))
|
|
118
|
+
|
|
119
|
+
transaction = CfdpSourceTransaction.new
|
|
120
|
+
transaction.load_state("1__123")
|
|
121
|
+
expect(transaction.complete_time).to eq(Time.parse("2023-01-01T10:00:00.000000Z"))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
100
125
|
describe "save_state optimization during file PDU transmission" do
|
|
101
126
|
before(:each) do
|
|
102
127
|
mock_redis()
|
|
@@ -370,6 +370,15 @@ RSpec.describe CfdpTransaction do
|
|
|
370
370
|
expect(state_data).not_to have_key("file_status")
|
|
371
371
|
expect(state_data).not_to have_key("complete_time")
|
|
372
372
|
end
|
|
373
|
+
|
|
374
|
+
it "saves complete_time when the transaction has completed" do
|
|
375
|
+
mock_redis
|
|
376
|
+
transaction.instance_variable_set(:@id, "1__123")
|
|
377
|
+
transaction.abandon
|
|
378
|
+
|
|
379
|
+
state_data = JSON.parse(OpenC3::Store.get("#{@redis_prefix}cfdp_transaction_state:1__123"))
|
|
380
|
+
expect(Time.parse(state_data["complete_time"]).to_f).to be_within(0.001).of(transaction.complete_time.to_f)
|
|
381
|
+
end
|
|
373
382
|
end
|
|
374
383
|
|
|
375
384
|
describe "load_state" do
|
|
@@ -438,6 +447,38 @@ RSpec.describe CfdpTransaction do
|
|
|
438
447
|
expect(transaction.condition_code).to eq("NO_ERROR")
|
|
439
448
|
expect(transaction.metadata_pdu_count).to eq(0)
|
|
440
449
|
expect(transaction.instance_variable_get(:@fault_handler_overrides)).to eq({})
|
|
450
|
+
expect(transaction.complete_time).to be_nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it "loads complete_time" do
|
|
454
|
+
mock_redis
|
|
455
|
+
state_data = {"id" => "1__123", "state" => "FINISHED", "create_time" => "2023-01-01T10:00:00.000000Z",
|
|
456
|
+
"complete_time" => "2023-01-01T11:00:00.000000Z"}
|
|
457
|
+
OpenC3::Store.set("#{@redis_prefix}cfdp_transaction_state:1__123", JSON.generate(state_data))
|
|
458
|
+
|
|
459
|
+
transaction.load_state("1__123")
|
|
460
|
+
expect(transaction.complete_time).to eq(Time.parse("2023-01-01T11:00:00.000000Z"))
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
%w(FINISHED CANCELED ABANDONED).each do |state|
|
|
464
|
+
it "uses create_time as complete_time for a #{state} transaction saved without one" do
|
|
465
|
+
# State saved by older versions never included complete_time, so these were never cleaned up
|
|
466
|
+
mock_redis
|
|
467
|
+
state_data = {"id" => "1__123", "state" => state, "create_time" => "2023-01-01T10:00:00.000000Z"}
|
|
468
|
+
OpenC3::Store.set("#{@redis_prefix}cfdp_transaction_state:1__123", JSON.generate(state_data))
|
|
469
|
+
|
|
470
|
+
transaction.load_state("1__123")
|
|
471
|
+
expect(transaction.complete_time).to eq(Time.parse("2023-01-01T10:00:00.000000Z"))
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
it "leaves complete_time nil for an incomplete transaction" do
|
|
476
|
+
mock_redis
|
|
477
|
+
state_data = {"id" => "1__123", "state" => "SUSPENDED", "create_time" => "2023-01-01T10:00:00.000000Z"}
|
|
478
|
+
OpenC3::Store.set("#{@redis_prefix}cfdp_transaction_state:1__123", JSON.generate(state_data))
|
|
479
|
+
|
|
480
|
+
transaction.load_state("1__123")
|
|
481
|
+
expect(transaction.complete_time).to be_nil
|
|
441
482
|
end
|
|
442
483
|
end
|
|
443
484
|
|
|
@@ -483,7 +524,7 @@ RSpec.describe CfdpTransaction do
|
|
|
483
524
|
expect(new_transaction.metadata_pdu_hash).to eq("abc123")
|
|
484
525
|
expect(new_transaction.metadata_pdu_count).to eq(3)
|
|
485
526
|
expect(new_transaction.create_time.to_i).to eq(original_time.to_i)
|
|
486
|
-
expect(new_transaction.complete_time).to be_nil #
|
|
527
|
+
expect(new_transaction.complete_time).to be_nil # Not complete
|
|
487
528
|
expect(new_transaction.proxy_response_info).to eq("proxy_info")
|
|
488
529
|
expect(new_transaction.proxy_response_needed).to be true
|
|
489
530
|
expect(new_transaction.instance_variable_get(:@canceling_entity_id)).to eq(2)
|
|
@@ -61,6 +61,33 @@ RSpec.describe CfdpUser, type: :model do
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
describe "start" do
|
|
64
|
+
it "does not resume a new Put created during telemetry initialization" do
|
|
65
|
+
CfdpMib.source_entity['tlm_info'] = [['CFDP', 'PDU', 'DATA']]
|
|
66
|
+
transaction = double('new source transaction', copy_state: nil)
|
|
67
|
+
expect(transaction).not_to receive(:clear_shutdown)
|
|
68
|
+
expect(transaction).not_to receive(:copy_file)
|
|
69
|
+
allow(OpenC3::Topic).to receive(:update_topic_offsets) do
|
|
70
|
+
CfdpMib.transactions["#{CfdpMib.source_entity_id}__999"] = transaction
|
|
71
|
+
@user.instance_variable_set(:@cancel_thread, true)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@user.start.join(5)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "initializes telemetry offsets before resuming source transactions" do
|
|
78
|
+
CfdpMib.source_entity['tlm_info'] = [['CFDP', 'PDU', 'DATA']]
|
|
79
|
+
events = []
|
|
80
|
+
allow(OpenC3::Topic).to receive(:update_topic_offsets) { events << :offsets }
|
|
81
|
+
allow(@user).to receive(:resume_incomplete_source_transactions) do
|
|
82
|
+
events << :resume
|
|
83
|
+
@user.instance_variable_set(:@cancel_thread, true)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
@user.start.join(5)
|
|
87
|
+
|
|
88
|
+
expect(events).to eq([:offsets, :resume])
|
|
89
|
+
end
|
|
90
|
+
|
|
64
91
|
it "sleeps instead of spinning when tlm_info is not configured" do
|
|
65
92
|
# Store.read_topics returns immediately when given no topics, so the receive loop has to
|
|
66
93
|
# pace itself or it pins a CPU for the life of the process
|
|
@@ -81,6 +108,49 @@ RSpec.describe CfdpUser, type: :model do
|
|
|
81
108
|
expect(sleeps).to eq(2)
|
|
82
109
|
expect(OpenC3::Logger).to have_received(:error).with(/tlm_info is not configured/, anything)
|
|
83
110
|
end
|
|
111
|
+
|
|
112
|
+
it "cleans up old transactions on the first pass and then only every transaction_cleanup_frequency_hours" do
|
|
113
|
+
# A microservice restarted more often than the cleanup frequency must still clean up
|
|
114
|
+
CfdpMib.source_entity['tlm_info'] = []
|
|
115
|
+
allow(OpenC3::Logger).to receive(:error)
|
|
116
|
+
allow(CfdpMib).to receive(:cleanup_old_transactions)
|
|
117
|
+
|
|
118
|
+
sleeps = 0
|
|
119
|
+
allow(@user).to receive(:sleep) do |seconds|
|
|
120
|
+
next unless seconds == CfdpUser::READ_TOPICS_TIMEOUT_S
|
|
121
|
+
sleeps += 1
|
|
122
|
+
@user.instance_variable_set(:@cancel_thread, true) if sleeps >= 3
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@user.start.join(5)
|
|
126
|
+
|
|
127
|
+
expect(CfdpMib).to have_received(:cleanup_old_transactions).once
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "does not clean up transactions whose resumed source thread is still running" do
|
|
131
|
+
# A transaction canceled before a restart has an old complete_time but its source thread is
|
|
132
|
+
# resumed to finish sending EOF, so the first cleanup pass must not delete it out from under it
|
|
133
|
+
CfdpMib.source_entity['tlm_info'] = []
|
|
134
|
+
allow(OpenC3::Logger).to receive(:error)
|
|
135
|
+
cleanup_skip_ids = nil
|
|
136
|
+
allow(CfdpMib).to receive(:cleanup_old_transactions) { |skip_ids: []| cleanup_skip_ids = skip_ids }
|
|
137
|
+
|
|
138
|
+
release = Queue.new
|
|
139
|
+
transaction = double("transaction", id: "1__5", request_shutdown: nil, save_state: nil)
|
|
140
|
+
allow(@user).to receive(:resume_incomplete_source_transactions) do
|
|
141
|
+
@user.register_source_transaction(transaction, Thread.new { release.pop })
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
allow(@user).to receive(:sleep) do |seconds|
|
|
145
|
+
next unless seconds == CfdpUser::READ_TOPICS_TIMEOUT_S
|
|
146
|
+
@user.instance_variable_set(:@cancel_thread, true)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
@user.start.join(5)
|
|
150
|
+
release << true
|
|
151
|
+
|
|
152
|
+
expect(cleanup_skip_ids).to eq(["1__5"])
|
|
153
|
+
end
|
|
84
154
|
end
|
|
85
155
|
|
|
86
156
|
describe "update_transactions" do
|
|
@@ -98,6 +168,26 @@ RSpec.describe CfdpUser, type: :model do
|
|
|
98
168
|
expect(good).to have_received(:update)
|
|
99
169
|
expect(OpenC3::Logger).to have_received(:error).with(/1__1 update failed/, anything)
|
|
100
170
|
end
|
|
171
|
+
|
|
172
|
+
it "persists that the proxy put response was sent" do
|
|
173
|
+
# The completed transaction is saved with proxy_response_needed set, so a restart would send
|
|
174
|
+
# the response again unless clearing it is saved too
|
|
175
|
+
transaction = CfdpSourceTransaction.new
|
|
176
|
+
transaction.proxy_response_info = { "SOURCE_ENTITY_ID" => @destination_entity_id, "SEQUENCE_NUMBER" => 5 }
|
|
177
|
+
transaction.notice_of_completion
|
|
178
|
+
allow(transaction).to receive(:update)
|
|
179
|
+
pdu = double("pdu", build_proxy_put_response_message: "response", build_originating_transaction_id_message: "originating")
|
|
180
|
+
allow(CfdpPdu).to receive(:build_initial_pdu).and_return(pdu)
|
|
181
|
+
allow(@user).to receive(:start_source_transaction)
|
|
182
|
+
|
|
183
|
+
@user.update_transactions
|
|
184
|
+
|
|
185
|
+
expect(@user).to have_received(:start_source_transaction).once
|
|
186
|
+
loaded = CfdpSourceTransaction.new
|
|
187
|
+
loaded.load_state(transaction.id)
|
|
188
|
+
expect(loaded.proxy_response_needed).to be false
|
|
189
|
+
expect(loaded.proxy_response_info).to be_nil
|
|
190
|
+
end
|
|
101
191
|
end
|
|
102
192
|
|
|
103
193
|
describe "reap_source_transactions" do
|
|
@@ -1014,4 +1104,4 @@ RSpec.describe CfdpUser, type: :model do
|
|
|
1014
1104
|
expect(@user.instance_variable_get(:@source_transactions)).to eq([[transaction, source_thread]])
|
|
1015
1105
|
end
|
|
1016
1106
|
end
|
|
1017
|
-
end
|
|
1107
|
+
end
|
data/plugin.txt
CHANGED
|
@@ -59,7 +59,7 @@ VARIABLE allow_duplicate_transaction_ids false
|
|
|
59
59
|
VARIABLE_STATE "TRUE" true
|
|
60
60
|
VARIABLE_STATE "FALSE" false
|
|
61
61
|
VARIABLE transaction_cleanup_frequency_hours 24
|
|
62
|
-
VARIABLE_DESCRIPTION "How often to purge
|
|
62
|
+
VARIABLE_DESCRIPTION "How often (and at startup) to purge completed transactions older than the source entity transaction_retain_seconds (default 86400) from RAM and Redis"
|
|
63
63
|
|
|
64
64
|
VARIABLE plugin_test_mode false
|
|
65
65
|
VARIABLE_DESCRIPTION "Install 2 targets: CFDP and CFDP2 along with the CFDP2 microservice configured to talk to CFDP2 target for testing purposes."
|
data/pyproject.toml
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openc3-cosmos-cfdp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Melton
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-11-13 00:00:00.000000000 Z
|
|
13
13
|
dependencies: []
|
|
14
14
|
description: This plugin provides COSMOS Support for CCSDS File Delivery Protocol
|
|
15
15
|
(CFDP). It includes a COSMOS API for sending CFDP commands and an example COSMOS
|