github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/internal/events/batch_pin_complete.go (about) 1 // Copyright © 2021 Kaleido, Inc. 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 package events 18 19 import ( 20 "context" 21 "encoding/json" 22 "io" 23 24 "github.com/kaleido-io/firefly/internal/log" 25 "github.com/kaleido-io/firefly/pkg/blockchain" 26 "github.com/kaleido-io/firefly/pkg/database" 27 "github.com/kaleido-io/firefly/pkg/fftypes" 28 ) 29 30 // BatchPinComplete is called in-line with a particular ledger's stream of events, so while we 31 // block here this blockchain event remains un-acknowledged, and no further events will arrive from this 32 // particular ledger. 33 // 34 // We must block here long enough to get the payload from the publicstorage, persist the messages in the correct 35 // sequence, and also persist all the data. 36 func (em *eventManager) BatchPinComplete(bi blockchain.Plugin, batchPin *blockchain.BatchPin, signingIdentity string, protocolTxID string, additionalInfo fftypes.JSONObject) error { 37 38 log.L(em.ctx).Infof("-> BatchPinComplete txn=%s author=%s", protocolTxID, signingIdentity) 39 defer func() { 40 log.L(em.ctx).Infof("<- BatchPinComplete txn=%s author=%s", protocolTxID, signingIdentity) 41 }() 42 log.L(em.ctx).Tracef("BatchPinComplete info: %+v", additionalInfo) 43 44 if batchPin.BatchPaylodRef != nil { 45 return em.handleBroadcastPinComplete(batchPin, signingIdentity, protocolTxID, additionalInfo) 46 } 47 return em.handlePrivatePinComplete(batchPin, signingIdentity, protocolTxID, additionalInfo) 48 } 49 50 func (em *eventManager) handlePrivatePinComplete(batchPin *blockchain.BatchPin, signingIdentity string, protocolTxID string, additionalInfo fftypes.JSONObject) error { 51 // Here we simple record all the pins as parked, and emit an event for the aggregator 52 // to check whether the messages in the batch have been written. 53 return em.retry.Do(em.ctx, "persist private batch pins", func(attempt int) (bool, error) { 54 // We process the batch into the DB as a single transaction (if transactions are supported), both for 55 // efficiency and to minimize the chance of duplicates (although at-least-once delivery is the core model) 56 err := em.database.RunAsGroup(em.ctx, func(ctx context.Context) error { 57 err := em.persistBatchTransaction(ctx, batchPin, signingIdentity, protocolTxID, additionalInfo) 58 if err == nil { 59 err = em.persistContexts(ctx, batchPin, true) 60 } 61 return err 62 }) 63 return err != nil, err // retry indefinitely (until context closes) 64 }) 65 } 66 67 func (em *eventManager) persistBatchTransaction(ctx context.Context, batchPin *blockchain.BatchPin, signingIdentity string, protocolTxID string, additionalInfo fftypes.JSONObject) error { 68 // Get any existing record for the batch transaction record 69 tx, err := em.database.GetTransactionByID(ctx, batchPin.TransactionID) 70 if err != nil { 71 return err // a peristence failure here is considered retryable (so returned) 72 } 73 if err := fftypes.ValidateFFNameField(ctx, batchPin.Namespace, "namespace"); err != nil { 74 log.L(ctx).Errorf("Invalid batch '%s'. Transaction '%s' invalid namespace '%s': %a", batchPin.BatchID, batchPin.TransactionID, batchPin.Namespace, err) 75 return nil // This is not retryable. skip this batch 76 } 77 if tx == nil { 78 // We're the first to write the transaction record on this node 79 tx = &fftypes.Transaction{ 80 ID: batchPin.TransactionID, 81 Subject: fftypes.TransactionSubject{ 82 Namespace: batchPin.Namespace, 83 Type: fftypes.TransactionTypeBatchPin, 84 Signer: signingIdentity, 85 Reference: batchPin.BatchID, 86 }, 87 Created: fftypes.Now(), 88 } 89 tx.Hash = tx.Subject.Hash() 90 } else if tx.Subject.Type != fftypes.TransactionTypeBatchPin || 91 tx.Subject.Signer != signingIdentity || 92 tx.Subject.Reference == nil || 93 *tx.Subject.Reference != *batchPin.BatchID || 94 tx.Subject.Namespace != batchPin.Namespace { 95 log.L(ctx).Errorf("Invalid batch '%s'. Existing transaction '%s' does not match batch subject", batchPin.BatchID, tx.ID) 96 return nil // This is not retryable. skip this batch 97 } 98 99 // Set the updates on the transaction 100 tx.ProtocolID = protocolTxID 101 tx.Info = additionalInfo 102 tx.Status = fftypes.OpStatusSucceeded 103 104 // Upsert the transaction, ensuring the hash does not change 105 err = em.database.UpsertTransaction(ctx, tx, true, false) 106 if err != nil { 107 if err == database.HashMismatch { 108 log.L(ctx).Errorf("Invalid batch '%s'. Transaction '%s' hash mismatch with existing record", batchPin.BatchID, tx.Hash) 109 return nil // This is not retryable. skip this batch 110 } 111 log.L(ctx).Errorf("Failed to insert transaction for batch '%s': %s", batchPin.BatchID, err) 112 return err // a peristence failure here is considered retryable (so returned) 113 } 114 115 return nil 116 } 117 118 func (em *eventManager) persistContexts(ctx context.Context, batchPin *blockchain.BatchPin, private bool) error { 119 for idx, hash := range batchPin.Contexts { 120 if err := em.database.UpsertPin(ctx, &fftypes.Pin{ 121 Masked: private, 122 Hash: hash, 123 Batch: batchPin.BatchID, 124 Index: int64(idx), 125 Created: fftypes.Now(), 126 }); err != nil { 127 return err 128 } 129 } 130 return nil 131 } 132 133 func (em *eventManager) handleBroadcastPinComplete(batchPin *blockchain.BatchPin, signingIdentity string, protocolTxID string, additionalInfo fftypes.JSONObject) error { 134 var body io.ReadCloser 135 if err := em.retry.Do(em.ctx, "retrieve data", func(attempt int) (retry bool, err error) { 136 body, err = em.publicstorage.RetrieveData(em.ctx, batchPin.BatchPaylodRef) 137 return err != nil, err // retry indefinitely (until context closes) 138 }); err != nil { 139 return err 140 } 141 defer body.Close() 142 143 var batch *fftypes.Batch 144 err := json.NewDecoder(body).Decode(&batch) 145 if err != nil { 146 log.L(em.ctx).Errorf("Failed to parse payload referred in batch ID '%s' from transaction '%s'", batchPin.BatchID, protocolTxID) 147 return nil // log and swallow unprocessable data 148 } 149 body.Close() 150 151 // At this point the batch is parsed, so any errors in processing need to be considered as: 152 // 1) Retryable - any transient error returned by processBatch is retried indefinitely 153 // 2) Swallowable - the data is invalid, and we have to move onto subsequent messages 154 // 3) Server shutting down - the context is cancelled (handled by retry) 155 return em.retry.Do(em.ctx, "persist batch", func(attempt int) (bool, error) { 156 // We process the batch into the DB as a single transaction (if transactions are supported), both for 157 // efficiency and to minimize the chance of duplicates (although at-least-once delivery is the core model) 158 err := em.database.RunAsGroup(em.ctx, func(ctx context.Context) error { 159 err := em.persistBatchTransaction(ctx, batchPin, signingIdentity, protocolTxID, additionalInfo) 160 if err == nil { 161 err = em.persistBatchFromBroadcast(ctx, batch, batchPin.BatchHash, signingIdentity) 162 if err == nil { 163 err = em.persistContexts(ctx, batchPin, false) 164 } 165 } 166 return err 167 }) 168 return err != nil, err // retry indefinitely (until context closes) 169 }) 170 }