synapsecns/sanguine

View on GitHub
ethergo/util/chunk.go

Summary

Maintainability
B
5 hrs
Test Coverage
package util

import (
    "github.com/synapsecns/sanguine/core"
    "math/big"
    "sync"
)

// Chunk represents an individual chunk of startBlock-startBlock.
// TODO: this needs to be moved to scribe.
type Chunk struct {
    // StartBlock for this chunk. It is less then end block in ascending txes and greater in descending.
    StartBlock *big.Int
    // EndBlock for this chunk
    EndBlock *big.Int
    // ascending is whether or not the chunk was made in ascending mode. This is used
    // for min/max without a comparotor operation
    ascending bool
}

// MinBlock returns the minimum of start block and end block. This is useful because start/end change
// based on the ordering of the chunk.
func (c *Chunk) MinBlock() *big.Int {
    if c.ascending {
        return c.StartBlock
    }
    return c.EndBlock
}

// MaxBlock returns the minimum of start block and end block. This is useful because start/end change
// based on the ordering of the chunk.
func (c *Chunk) MaxBlock() *big.Int {
    if c.ascending {
        return c.EndBlock
    }
    return c.StartBlock
}

// baseChunkIterator contains a non-directional chunk iterator.
type baseChunkIterator struct {
    // chunkSize is the chunk size
    chunkSize int
    // mux locks the iterator to prevent concurrent issues
    //nolint: structcheck // triggers a false positive. This is used in ascendingChunkIterator and descendingChunkIterator
    mux sync.Mutex
}

// ascendingChunkIterator returns an iterator going from lowest -> highest.
type ascendingChunkIterator struct {
    *baseChunkIterator
    // endBlock is the last block to chunk at
    endBlock *big.Int
    // lastIteratedBlock is the most recent iterated block. This will always be less then endBlock
    lastIteratedBlock *big.Int
}

// descendingChunkIterator returns an iterator going from highest->lowest.
type descendingChunkIterator struct {
    *baseChunkIterator
    // startBlock is the last block to chunk at
    startBlock *big.Int
    // lastIteratedBlock is the block we most recently iterator over. This will always be greater then start block
    lastIteratedBlock *big.Int
}

// ChunkIterator is an instantiation of a stateful iterator that groups startBlock-endBlock blocks into length
// chunkSize.
type ChunkIterator interface {
    // NextChunk gets the next chunk. If the iterator has completed null will be returned.
    NextChunk() *Chunk
}

// NewChunkIterator creates an iterator for a range of elements (startBlock-endBlock) split into groups the length of chunkSize
// the final chunk has any remaining elements. An iterator is used here as opposed to a slice to save memory while iterating over *very
// large* chunks.This follows the stateful iterator pattern: https://ewencp.org/blog/golang-iterators/index.html
func NewChunkIterator(startBlock, endBlock *big.Int, chunkSize int, ascending bool) ChunkIterator {
    baseIterator := &baseChunkIterator{
        chunkSize: chunkSize,
    }

    if ascending {
        return &ascendingChunkIterator{
            baseChunkIterator: baseIterator,
            endBlock:          core.CopyBigInt(endBlock),
            // lastIteratedBlock is set to StartBlock - 1
            lastIteratedBlock: big.NewInt(0).Sub(startBlock, big.NewInt(1)),
        }
    }

    return &descendingChunkIterator{
        baseChunkIterator: baseIterator,
        startBlock:        core.CopyBigInt(startBlock),
        // lastIteratedBlock is set to endBlock + 1 since we subtract one on every chunk
        lastIteratedBlock: big.NewInt(0).Add(endBlock, big.NewInt(1)),
    }
}

// NextChunk gets the next chunk in descending order. Returns nil if complete.
func (d *descendingChunkIterator) NextChunk() *Chunk {
    d.mux.Lock()
    defer d.mux.Unlock()

    startBlock := big.NewInt(0).Sub(d.lastIteratedBlock, big.NewInt(1))
    // if the last block is greater than the start block, we're done
    if startBlock.Cmp(d.startBlock) < 0 {
        return nil
    }

    endBlock := big.NewInt(0).Sub(startBlock, big.NewInt(int64(d.chunkSize)))

    // if the last block is less then the end block, return the last block
    if endBlock.Cmp(d.startBlock) < 0 {
        endBlock = core.CopyBigInt(d.startBlock)
    }

    d.lastIteratedBlock = core.CopyBigInt(endBlock)

    return &Chunk{
        StartBlock: startBlock,
        EndBlock:   endBlock,
        ascending:  false,
    }
}

// NextChunk gets the next chunk in ascending order. Returns nil if complete.
func (c *ascendingChunkIterator) NextChunk() *Chunk {
    c.mux.Lock()
    defer c.mux.Unlock()

    startBlock := big.NewInt(0).Add(c.lastIteratedBlock, big.NewInt(1))
    // if the last block is greater than the start block, we're done
    if startBlock.Cmp(c.endBlock) > 0 {
        return nil
    }

    endBlock := big.NewInt(0).Add(startBlock, big.NewInt(int64(c.chunkSize)))

    // if the last block is greater then the end block, return the last block
    if endBlock.Cmp(c.endBlock) > 0 {
        endBlock = core.CopyBigInt(c.endBlock)
    }

    c.lastIteratedBlock = core.CopyBigInt(endBlock)

    return &Chunk{
        StartBlock: startBlock,
        EndBlock:   endBlock,
        ascending:  true,
    }
}