github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/internal/input/orderbooks.go (about)

     1  package input
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"math"
     9  	"sync"
    10  
    11  	"github.com/stellar/go/ingest"
    12  	"github.com/stellar/go/ingest/ledgerbackend"
    13  	"github.com/stellar/stellar-etl/internal/transform"
    14  	"github.com/stellar/stellar-etl/internal/utils"
    15  )
    16  
    17  // OrderbookBatch represents a batch of orderbooks
    18  type OrderbookBatch struct {
    19  	BatchStart uint32
    20  	BatchEnd   uint32
    21  	Orderbooks map[uint32][]ingest.Change
    22  }
    23  
    24  // OrderbookParser handles parsing orderbooks
    25  type OrderbookParser struct {
    26  	Events            [][]byte
    27  	Markets           [][]byte
    28  	SeenMarketHashes  map[uint64]bool
    29  	Offers            [][]byte
    30  	SeenOfferHashes   map[uint64]bool
    31  	Accounts          [][]byte
    32  	SeenAccountHashes map[uint64]bool
    33  	Logger            *utils.EtlLogger
    34  }
    35  
    36  // convertOffer converts an offer to its normalized form and adds it to the AllConvertedOffers
    37  func (o *OrderbookParser) convertOffer(allConvertedOffers []transform.NormalizedOfferOutput, index int, offer ingest.Change, seq uint32, wg *sync.WaitGroup) {
    38  	defer wg.Done()
    39  	transformed, err := transform.TransformOfferNormalized(offer, seq)
    40  	if err != nil {
    41  		errorMsg := fmt.Errorf("error json marshalling offer #%d in ledger sequence number #%d: %s", index, seq, err)
    42  		o.Logger.LogError(errorMsg)
    43  	} else {
    44  		allConvertedOffers[index] = transformed
    45  	}
    46  }
    47  
    48  // NewOrderbookParser creates a new orderbook parser and returns it
    49  func NewOrderbookParser(logger *utils.EtlLogger) OrderbookParser {
    50  	return OrderbookParser{
    51  		Events:            make([][]byte, 0),
    52  		Markets:           make([][]byte, 0),
    53  		SeenMarketHashes:  make(map[uint64]bool),
    54  		Offers:            make([][]byte, 0),
    55  		SeenOfferHashes:   make(map[uint64]bool),
    56  		Accounts:          make([][]byte, 0),
    57  		SeenAccountHashes: make(map[uint64]bool),
    58  		Logger:            logger,
    59  	}
    60  }
    61  
    62  
    63  func (o *OrderbookParser) parseOrderbook(orderbook []ingest.Change, seq uint32) {
    64  	var group sync.WaitGroup
    65  	allConverted := make([]transform.NormalizedOfferOutput, len(orderbook))
    66  	for i, v := range orderbook {
    67  		group.Add(1)
    68  		go o.convertOffer(allConverted, i, v, seq, &group)
    69  	}
    70  
    71  	group.Wait()
    72  
    73  	for _, converted := range allConverted {
    74  		if _, exists := o.SeenMarketHashes[converted.Market.ID]; !exists {
    75  			o.SeenMarketHashes[converted.Market.ID] = true
    76  			marshalledMarket, err := json.Marshal(converted.Market)
    77  			if err != nil {
    78  				errorMsg := fmt.Errorf("error json marshalling market for offer  %d: %s", converted.Offer.HorizonID, err)
    79  				o.Logger.LogError(errorMsg)
    80  				continue
    81  			}
    82  
    83  			o.Markets = append(o.Markets, marshalledMarket)
    84  		}
    85  
    86  		if _, exists := o.SeenAccountHashes[converted.Account.ID]; !exists {
    87  			o.SeenAccountHashes[converted.Account.ID] = true
    88  			marshalledAccount, err := json.Marshal(converted.Account)
    89  			if err != nil {
    90  				errorMsg := fmt.Errorf("error json marshalling account for offer  %d: %s", converted.Offer.HorizonID, err)
    91  				o.Logger.LogError(errorMsg)
    92  				continue
    93  			}
    94  
    95  			o.Accounts = append(o.Accounts, marshalledAccount)
    96  		}
    97  
    98  		if _, exists := o.SeenOfferHashes[converted.Offer.DimOfferID]; !exists {
    99  			o.SeenOfferHashes[converted.Offer.DimOfferID] = true
   100  			marshalledOffer, err := json.Marshal(converted.Offer)
   101  			if err != nil {
   102  				errorMsg := fmt.Errorf("error json marshalling offer %d: %s", converted.Offer.HorizonID, err)
   103  				o.Logger.LogError(errorMsg)
   104  				continue
   105  			}
   106  
   107  			o.Offers = append(o.Offers, marshalledOffer)
   108  
   109  		}
   110  
   111  		marshalledEvent, err := json.Marshal(converted.Event)
   112  		if err != nil {
   113  			errorMsg := fmt.Errorf("error json marshalling event for offer %d: %s", converted.Offer.HorizonID, err)
   114  			o.Logger.LogError(errorMsg)
   115  			continue
   116  		} else {
   117  			o.Events = append(o.Events, marshalledEvent)
   118  		}
   119  	}
   120  }
   121  
   122  // GetOfferChanges gets the offer changes that ocurred between the firstSeq ledger and nextSeq ledger
   123  func GetOfferChanges(core *ledgerbackend.CaptiveStellarCore, env utils.EnvironmentDetails, firstSeq, nextSeq uint32) (*ingest.ChangeCompactor, error) {
   124  	offChanges := ingest.NewChangeCompactor()
   125  	ctx := context.Background()
   126  
   127  	for seq := firstSeq; seq <= nextSeq; {
   128  		latestLedger, err := core.GetLatestLedgerSequence(ctx)
   129  		if err != nil {
   130  			return nil, fmt.Errorf(fmt.Sprintf("unable to get latest ledger at ledger %d: ", seq), err)
   131  		}
   132  
   133  		// if this ledger is available, we can read its changes and move on to the next ledger by incrementing seq.
   134  		// Otherwise, nothing is incremented and we try again on the next iteration of the loop
   135  		if seq <= latestLedger {
   136  			changeReader, err := ingest.NewLedgerChangeReader(ctx, core, env.NetworkPassphrase, seq)
   137  			if err != nil {
   138  				return nil, fmt.Errorf(fmt.Sprintf("unable to create change reader for ledger %d: ", seq), err)
   139  			}
   140  
   141  			for {
   142  				change, err := changeReader.Read()
   143  				if err == io.EOF {
   144  					break
   145  				}
   146  
   147  				if err != nil {
   148  					return nil, fmt.Errorf(fmt.Sprintf("unable to read changes from ledger %d: ", seq), err)
   149  				}
   150  				offChanges.AddChange(change)
   151  			}
   152  
   153  			changeReader.Close()
   154  			seq++
   155  		}
   156  	}
   157  
   158  	return offChanges, nil
   159  }
   160  
   161  func exportOrderbookBatch(batchStart, batchEnd uint32, core *ledgerbackend.CaptiveStellarCore, orderbookChan chan OrderbookBatch, startOrderbook []ingest.Change, env utils.EnvironmentDetails, logger *utils.EtlLogger) {
   162  	batchMap := make(map[uint32][]ingest.Change)
   163  	batchMap[batchStart] = make([]ingest.Change, len(startOrderbook))
   164  	copy(batchMap[batchStart], startOrderbook)
   165  	ctx := context.Background()
   166  
   167  	prevSeq := batchStart
   168  	curSeq := batchStart + 1
   169  	for curSeq < batchEnd {
   170  		latestLedger, err := core.GetLatestLedgerSequence(ctx)
   171  		if err != nil {
   172  			logger.Fatal("unable to get the lastest ledger sequence: ", err)
   173  		}
   174  
   175  		// if this ledger is available, we process its changes and move on to the next ledger by incrementing seq.
   176  		// Otherwise, nothing is incremented and we try again on the next iteration of the loop
   177  		if curSeq <= latestLedger {
   178  			UpdateOrderbook(prevSeq, curSeq, startOrderbook, core, env, logger)
   179  			batchMap[curSeq] = make([]ingest.Change, len(startOrderbook))
   180  			copy(batchMap[curSeq], startOrderbook)
   181  			prevSeq = curSeq
   182  			curSeq++
   183  		}
   184  	}
   185  
   186  	batch := OrderbookBatch{
   187  		BatchStart: batchStart,
   188  		BatchEnd:   batchEnd,
   189  		Orderbooks: batchMap,
   190  	}
   191  
   192  	orderbookChan <- batch
   193  }
   194  
   195  // UpdateOrderbook updates an orderbook at ledger start to its state at ledger end
   196  func UpdateOrderbook(start, end uint32, orderbook []ingest.Change, core *ledgerbackend.CaptiveStellarCore, env utils.EnvironmentDetails, logger *utils.EtlLogger) {
   197  	if start > end {
   198  		logger.Fatalf("unable to update orderbook start ledger %d is after end %d: ", start, end)
   199  	}
   200  
   201  	changeCache, err := GetOfferChanges(core, env, start, end)
   202  	if err != nil {
   203  		logger.Fatal(fmt.Sprintf("unable to get offer changes between ledger %d and %d: ", start, end), err)
   204  	}
   205  
   206  	for _, change := range orderbook {
   207  		changeCache.AddChange(change)
   208  	}
   209  
   210  	orderbook = changeCache.GetChanges()
   211  }
   212  
   213  // StreamOrderbooks exports all the batches of orderbooks between start and end to the orderbookChannel. If end is 0, then it exports in an unbounded fashion
   214  func StreamOrderbooks(core *ledgerbackend.CaptiveStellarCore, start, end, batchSize uint32, orderbookChannel chan OrderbookBatch, startOrderbook []ingest.Change, env utils.EnvironmentDetails, logger *utils.EtlLogger) {
   215  	// The initial orderbook is at the checkpoint sequence, not the start of the range, so it needs to be updated
   216  	checkpointSeq := utils.GetMostRecentCheckpoint(start)
   217  	UpdateOrderbook(checkpointSeq, start, startOrderbook, core, env, logger)
   218  
   219  	if end != 0 {
   220  		totalBatches := uint32(math.Ceil(float64(end-start+1) / float64(batchSize)))
   221  		for currentBatch := uint32(0); currentBatch < totalBatches; currentBatch++ {
   222  			batchStart := start + currentBatch*batchSize
   223  			batchEnd := batchStart + batchSize
   224  			if batchEnd > end+1 {
   225  				batchEnd = end + 1
   226  			}
   227  
   228  			exportOrderbookBatch(batchStart, batchEnd, core, orderbookChannel, startOrderbook, env, logger)
   229  		}
   230  	} else {
   231  		batchStart := start
   232  		batchEnd := batchStart + batchSize
   233  		for {
   234  			exportOrderbookBatch(batchStart, batchEnd, core, orderbookChannel, startOrderbook, env, logger)
   235  			batchStart = batchEnd
   236  			batchEnd = batchStart + batchSize
   237  		}
   238  	}
   239  }
   240  
   241  // ReceiveParsedOrderbooks reads a batch from the orderbookChannel, parses it using an orderbook parser, and returns the parser.
   242  func ReceiveParsedOrderbooks(orderbookChannel chan OrderbookBatch, logger *utils.EtlLogger) *OrderbookParser {
   243  	batchParser := NewOrderbookParser(logger)
   244  	batchRead := false
   245  	for {
   246  		select {
   247  		case batch, ok := <-orderbookChannel:
   248  			// if ok is false, it means the channel is closed. There will be no more batches, so we can set the channel to nil
   249  			if !ok {
   250  				orderbookChannel = nil
   251  				break
   252  			}
   253  
   254  			for seq, orderbook := range batch.Orderbooks {
   255  				batchParser.parseOrderbook(orderbook, seq)
   256  			}
   257  
   258  			batchRead = true
   259  		}
   260  
   261  		//if we have read a batch or the channel is closed, then break
   262  		if batchRead || orderbookChannel == nil {
   263  			break
   264  		}
   265  	}
   266  
   267  	return &batchParser
   268  }