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 }