Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC-0001: Rfc 0001 impl #1069

Merged
merged 9 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions exporters/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package exporters

import (
"github.com/algorand/go-algorand/agreement"
"github.com/algorand/go-algorand/data/bookkeeping"
"github.com/algorand/go-algorand/ledger/ledgercore"
)

// ExporterConfig is a generic string which can be deserialized by each individual Exporter.
type ExporterConfig string

// ExportData is the interface which all data types sent to Exporters should implement
type ExportData interface {
Round() uint64
}

// BlockExportData is provided to the Exporter on each round.
type BlockExportData struct {
// Block is the block data written to the blockchain.
Block bookkeeping.Block

// Delta contains a list of account changes resulting from the block. Processor plugins may have modify this data.
Delta ledgercore.StateDelta

// Certificate contains voting data that certifies the block. The certificate is non deterministic, a node stops collecting votes once the voting threshold is reached.
Certificate agreement.Certificate
}

// Round returns the round to which the BlockExportData corresponds
func (blkData *BlockExportData) Round() uint64 {
return uint64(blkData.Block.Round())
}

// ExporterMetadata contains fields relevant to identification and description of plugins.
type ExporterMetadata struct {
Name string
Description string
Deprecated bool
}

// Exporter defines the interface for plugins
type Exporter interface {
// Metadata associated with each Exporter.
Metadata() ExporterMetadata
Comment on lines +43 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do something like this, then we can use the same interface for the other plugins

type PluginMetadata interface {
	// Metadata associated with each Exporter.
	Metadata() ExporterMetadata
}
Suggested change
// Metadata associated with each Exporter.
Metadata() ExporterMetadata
PluginMetadata

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, it seems correct to use the same Metadata construct for all of the plugin types. I'll update this in a subsequent PR to make it more generic. It should probably end up being moved to a separate module in that case as well.


// Connect will be called during initialization, before block data starts going through the pipeline.
// Typically used for things like initializing network connections.
// The ExporterConfig passed to Connect will contain the Unmarhsalled config file specific to this plugin.
// Should return an error if it fails--this will result in the Indexer process terminating.
Connect(cfg ExporterConfig) error

// Config returns the configuration options used to create an Exporter.
// Initialized during Connect, it should return nil until the Exporter has been Connected.
Config() ExporterConfig

// Disconnect will be called during termination of the Indexer process.
// There is no guarantee that plugin lifecycle hooks will be invoked in any specific order in relation to one another.
// Returns an error if it fails which will be surfaced in the logs, but the process is already terminating.
Disconnect() error

// Receive is called for each block to be processed by the exporter.
// Should return an error on failure--retries are configurable.
Receive(exportData ExportData) error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main part I'm not sure about is whether we should have this abstraction in the first iteration. It means there will be a required type check in all plugins to convert it into BlockExportData.

I think it's ok to leave it like this for now, but lets keep an eye on it.


// HandleGenesis is an Exporter's opportunity to do initial validation and handling of the Genesis block.
// If validation (such as a check to ensure `genesis` matches a previously stored genesis block) or handling fails,
// it returns an error.
HandleGenesis(genesis bookkeeping.Genesis) error

// Round returns the next round not yet processed by the Exporter. Atomically updated when Receive successfully completes.
Round() uint64
}
40 changes: 40 additions & 0 deletions exporters/exporter_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package exporters

import (
"fmt"
)

// ExporterConstructor must be implemented by each Exporter.
// It provides a basic no-arg constructor for instances of an ExporterImpl.
type ExporterConstructor interface {
// New should return an instantiation of an Exporter.
// Configuration values should be passed and can be processed during `Connect()`.
New() Exporter
}

// exporterImpls is a k/v store from exporter names to their constructor implementations.
// This layer of indirection allows for different exporter integrations to be compiled in or compiled out by `go build --tags ...`
var exporterImpls = make(map[string]ExporterConstructor)

// RegisterExporter is used to register ExporterConstructor implementations. This mechanism allows
// for loose coupling between the configuration and the implementation. It is extremely similar to the way sql.DB
// driver's are configured and used.
func RegisterExporter(name string, constructor ExporterConstructor) {
exporterImpls[name] = constructor
}

// ExporterByName is used to construct an Exporter object by name.
// Returns an Exporter object, an availability channel that closes when the database
// becomes available, and an error object.
func ExporterByName(name string, cfg ExporterConfig) (Exporter, error) {
var constructor ExporterConstructor
var ok bool
if constructor, ok = exporterImpls[name]; !ok {
return nil, fmt.Errorf("no Exporter Constructor for %s", name)
}
val := constructor.New()
if err := val.Connect(cfg); err != nil {
return nil, err
}
return val, nil
}
51 changes: 51 additions & 0 deletions exporters/exporter_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package exporters

import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
)

type mockExporter struct {
mock.Mock
Exporter
}

func (m *mockExporter) Connect(config ExporterConfig) error {
args := m.Called(config)
return args.Error(0)
}

type mockExporterConstructor struct {
me *mockExporter
}

func (c *mockExporterConstructor) New() Exporter {
return c.me
}

func TestExporterByNameSuccess(t *testing.T) {
me := mockExporter{}
me.On("Connect", mock.Anything).Return(nil)
RegisterExporter("foobar", &mockExporterConstructor{&me})

exp, err := ExporterByName("foobar", "")
assert.NoError(t, err)
assert.Implements(t, (*Exporter)(nil), exp)
}

func TestExporterByNameNotFound(t *testing.T) {
_, err := ExporterByName("barfoo", "")
expectedErr := "no Exporter Constructor for barfoo"
assert.EqualError(t, err, expectedErr)
}

func TestExporterByNameConnectFailure(t *testing.T) {
me := mockExporter{}
expectedErr := fmt.Errorf("connect failure")
me.On("Connect", mock.Anything).Return(expectedErr)
RegisterExporter("baz", &mockExporterConstructor{&me})
_, err := ExporterByName("baz", "")
assert.EqualError(t, err, expectedErr.Error())
}
65 changes: 65 additions & 0 deletions exporters/noop/noop_exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package noop

import (
"github.com/algorand/go-algorand/data/bookkeeping"
"github.com/algorand/indexer/exporters"
)

// `noopExporter`s will function without ever erroring. This means they will also process out of order blocks
// which may or may not be desirable for different use cases--it can hide errors in actual exporters expecting in order
// block processing.
// The `noopExporter` will maintain `Round` state according to the round of the last block it processed.
type noopExporter struct {
round uint64
cfg exporters.ExporterConfig
}

var noopExporterMetadata exporters.ExporterMetadata = exporters.ExporterMetadata{
Name: "noop",
Description: "noop exporter",
Deprecated: false,
}

// Constructor is the ExporterConstructor implementation for the "noop" exporter
type Constructor struct{}

// New initializes a noopExporter
func (c *Constructor) New() exporters.Exporter {
return &noopExporter{
round: 0,
cfg: "",
}
}

func (exp *noopExporter) Metadata() exporters.ExporterMetadata {
return noopExporterMetadata
}

func (exp *noopExporter) Connect(_ exporters.ExporterConfig) error {
return nil
}

func (exp *noopExporter) Config() exporters.ExporterConfig {
return exp.cfg
}

func (exp *noopExporter) Disconnect() error {
return nil
}

func (exp *noopExporter) Receive(exportData exporters.ExportData) error {
exp.round = exportData.Round() + 1
return nil
}

func (exp *noopExporter) HandleGenesis(_ bookkeeping.Genesis) error {
return nil
}

func (exp *noopExporter) Round() uint64 {
return exp.round
}

func init() {
exporters.RegisterExporter(noopExporterMetadata.Name, &Constructor{})
}
55 changes: 55 additions & 0 deletions exporters/noop/noop_exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package noop

import (
"github.com/algorand/go-algorand/data/bookkeeping"
"github.com/algorand/indexer/exporters"
"github.com/stretchr/testify/assert"
"testing"
)

var nc = &Constructor{}

var ne = nc.New()

func TestExporterByName(t *testing.T) {
exporters.RegisterExporter(noopExporterMetadata.Name, nc)
ne, err := exporters.ExporterByName(noopExporterMetadata.Name, "")
assert.NoError(t, err)
assert.Implements(t, (*exporters.Exporter)(nil), ne)
}

func TestExporterMetadata(t *testing.T) {
meta := ne.Metadata()
assert.Equal(t, noopExporterMetadata.Name, meta.Name)
assert.Equal(t, noopExporterMetadata.Description, meta.Description)
assert.Equal(t, noopExporterMetadata.Deprecated, meta.Deprecated)
}

func TestExporterConnect(t *testing.T) {
assert.NoError(t, ne.Connect(""))
}

func TestExporterConfig(t *testing.T) {
assert.Equal(t, exporters.ExporterConfig(""), ne.Config())
}

func TestExporterDisconnect(t *testing.T) {
assert.NoError(t, ne.Disconnect())
}

func TestExporterHandleGenesis(t *testing.T) {
assert.NoError(t, ne.HandleGenesis(bookkeeping.Genesis{}))
}

func TestExporterRoundReceive(t *testing.T) {
eData := &exporters.BlockExportData{
Block: bookkeeping.Block{
BlockHeader: bookkeeping.BlockHeader{
Round: 5,
},
},
}
assert.Equal(t, uint64(0), ne.Round())
assert.NoError(t, ne.Receive(eData))
assert.Equal(t, uint64(6), ne.Round())
}