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  }