Repository: algorand/indexer
Pull request #203 (2020-09-22 15:18:58)

Add migration status to /health endpoint
Updates the /health endpoint with additional metadata relating to migrations.
Adds Health function to IndexerDb to provide status about internal migrations.
Adds a generic migration runner which is more testable (and tests for it).
Add some basic tests to the postgres backend to make sure migrations are attempted.


Patch:
diff --git a/api/error_messages.go b/api/error_messages.go
index b8374af..49c5729 100644
--- a/api/error_messages.go
+++ b/api/error_messages.go
@@ -1,6 +1,6 @@
 package api
 
-import(
+import (
 	"fmt"
 
 	"github.com/algorand/indexer/importer"
@@ -34,7 +34,6 @@ var errUnknownSigType string
 
 func init() {
 	errUnknownAddressRole = fmt.Sprintf("unknown address role [valid roles: %s]", AddressRoleEnumString)
-	errUnknownTxType      = fmt.Sprintf("unknown tx-type [valid types: %s]", importer.TypeEnumString)
-	errUnknownSigType     = fmt.Sprintf("unknown sig-type [valid types: %s]", SigTypeEnumString)
+	errUnknownTxType = fmt.Sprintf("unknown tx-type [valid types: %s]", importer.TypeEnumString)
+	errUnknownSigType = fmt.Sprintf("unknown sig-type [valid types: %s]", SigTypeEnumString)
 }
-
diff --git a/api/generated/common/routes.go b/api/generated/common/routes.go
index 961abd6..b98b09c 100644
--- a/api/generated/common/routes.go
+++ b/api/generated/common/routes.go
@@ -152,59 +152,60 @@ var swaggerSpec = []string{
 	"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",
-	"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",
-	"2dTxLwa4zraL24+3/z8AAP//tfyLkHWaAAA=",
+	"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",
+	"AwvMh/6aAAA=",
 }
 
 // GetSwagger returns the Swagger specification corresponding to the generated code
diff --git a/api/generated/common/types.go b/api/generated/common/types.go
index 23530a6..4b8204a 100644
--- a/api/generated/common/types.go
+++ b/api/generated/common/types.go
@@ -351,8 +351,11 @@ type EvalDeltaKeyValue struct {
 
 // HealthCheck defines model for HealthCheck.
 type HealthCheck struct {
-	Data    *map[string]interface{} `json:"data,omitempty"`
-	Message string                  `json:"message"`
+	Data          *map[string]interface{} `json:"data,omitempty"`
+	DbUnavailable bool                    `json:"db-unavailable"`
+	IsMigrating   bool                    `json:"is-migrating"`
+	Message       string                  `json:"message"`
+	Round         uint64                  `json:"round"`
 }
 
 // MiniAssetHolding defines model for MiniAssetHolding.
diff --git a/api/generated/v2/routes.go b/api/generated/v2/routes.go
index fde8c4e..688d3ca 100644
--- a/api/generated/v2/routes.go
+++ b/api/generated/v2/routes.go
@@ -1314,82 +1314,83 @@ var swaggerSpec = []string{
 	"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",
-	"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",
-	"jn7xLHTz6eb/BgAA//+mBRqHoOUAAA==",
+	"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",
+	"KeYAAA==",
 }
 
 // GetSwagger returns the Swagger specification corresponding to the generated code
diff --git a/api/generated/v2/types.go b/api/generated/v2/types.go
index 9844819..fd68122 100644
--- a/api/generated/v2/types.go
+++ b/api/generated/v2/types.go
@@ -351,8 +351,11 @@ type EvalDeltaKeyValue struct {
 
 // HealthCheck defines model for HealthCheck.
 type HealthCheck struct {
-	Data    *map[string]interface{} `json:"data,omitempty"`
-	Message string                  `json:"message"`
+	Data          *map[string]interface{} `json:"data,omitempty"`
+	DbUnavailable bool                    `json:"db-unavailable"`
+	IsMigrating   bool                    `json:"is-migrating"`
+	Message       string                  `json:"message"`
+	Round         uint64                  `json:"round"`
 }
 
 // MiniAssetHolding defines model for MiniAssetHolding.
diff --git a/api/handlers.go b/api/handlers.go
index 061be4d..e9afabc 100644
--- a/api/handlers.go
+++ b/api/handlers.go
@@ -58,12 +58,17 @@ const defaultBalancesLimit = 1000
 // Returns 200 if healthy.
 // (GET /health)
 func (si *ServerImplementation) MakeHealthCheck(ctx echo.Context) error {
-	maxRound, err := si.db.GetMaxRound()
+	health, err := si.db.Health()
 	if err != nil {
-		return indexerError(ctx, fmt.Sprintf("get max round: %v", err))
+		return indexerError(ctx, fmt.Sprintf("problem fetching health: %v", err))
 	}
+
 	return ctx.JSON(http.StatusOK, common.HealthCheckResponse{
-		Message: strconv.FormatUint(maxRound, 10),
+		Data:          health.Data,
+		Round:         health.Round,
+		IsMigrating:   health.IsMigrating,
+		DbUnavailable: health.DbUnavailable,
+		Message:       strconv.FormatUint(health.Round, 10),
 	})
 }
 
@@ -437,13 +442,12 @@ func (si *ServerImplementation) LookupTransactions(ctx echo.Context, txid string
 
 	response := generated.TransactionResponse{
 		CurrentRound: round,
-		Transaction: txns[0],
+		Transaction:  txns[0],
 	}
 
 	return ctx.JSON(http.StatusOK, response)
 }
 
-
 // SearchForTransactions returns transactions matching the provided parameters
 // (GET /v2/transactions)
 func (si *ServerImplementation) SearchForTransactions(ctx echo.Context, params generated.SearchForTransactionsParams) error {
diff --git a/api/indexer.oas2.json b/api/indexer.oas2.json
index 6a4694c..8be60a4 100644
--- a/api/indexer.oas2.json
+++ b/api/indexer.oas2.json
@@ -1154,12 +1154,24 @@
       "description": "A health check response.",
       "type": "object",
       "required": [
-        "message"
+        "message",
+        "round",
+        "is-migrating",
+        "db-unavailable"
       ],
       "properties": {
         "data": {
           "type": "object"
         },
+        "round": {
+          "type": "integer"
+        },
+        "is-migrating": {
+          "type": "boolean"
+        },
+        "db-unavailable": {
+          "type": "boolean"
+        },
         "message": {
           "type": "string"
         }
diff --git a/api/indexer.oas3.yml b/api/indexer.oas3.yml
index df040e9..bcf31b1 100644
--- a/api/indexer.oas3.yml
+++ b/api/indexer.oas3.yml
@@ -1041,12 +1041,24 @@
             "properties": {},
             "type": "object"
           },
+          "db-unavailable": {
+            "type": "boolean"
+          },
+          "is-migrating": {
+            "type": "boolean"
+          },
           "message": {
             "type": "string"
+          },
+          "round": {
+            "type": "integer"
           }
         },
         "required": [
-          "message"
+          "db-unavailable",
+          "is-migrating",
+          "message",
+          "round"
         ],
         "type": "object"
       },
diff --git a/api/middlewares/migration_middleware.go b/api/middlewares/migration_middleware.go
new file mode 100644
index 0000000..7eaad62
--- /dev/null
+++ b/api/middlewares/migration_middleware.go
@@ -0,0 +1,42 @@
+package middlewares
+
+import (
+	"net/http"
+
+	"github.com/labstack/echo/v4"
+
+	"github.com/algorand/indexer/idb"
+)
+
+// InProgressError is the error returned when a migration is in progress.
+var InProgressError = "Indexer migration in progress, please wait."
+
+// MigrationMiddleware makes sure a 500 error is returned when the IndexerDb has a migration in progress.
+type MigrationMiddleware struct {
+	idb idb.IndexerDb
+}
+
+// MakeMigrationMiddleware constructs the migration middleware
+func MakeMigrationMiddleware(idb idb.IndexerDb) echo.MiddlewareFunc {
+	mw := MigrationMiddleware{
+		idb: idb,
+	}
+
+	return mw.handler
+}
+
+// handler returns a 500 if the IndexerDb is migrating.
+func (mm *MigrationMiddleware) handler(next echo.HandlerFunc) echo.HandlerFunc {
+	return func(ctx echo.Context) error {
+		h, err := mm.idb.Health()
+		if err != nil {
+			return echo.NewHTTPError(http.StatusInternalServerError, "Indexer health error: %s", err.Error())
+		}
+
+		if h.IsMigrating {
+			return echo.NewHTTPError(http.StatusInternalServerError, InProgressError)
+		}
+
+		return next(ctx)
+	}
+}
diff --git a/api/server.go b/api/server.go
index 0f93cf8..d3248d7 100644
--- a/api/server.go
+++ b/api/server.go
@@ -29,10 +29,12 @@ func Serve(ctx context.Context, serveAddr string, db idb.IndexerDb, log *log.Log
 	e.Use(middlewares.MakeLogger(log))
 	e.Use(middleware.CORS())
 
+	middleware := make([]echo.MiddlewareFunc, 0)
+
+	middleware = append(middleware, middlewares.MakeMigrationMiddleware(db))
 
-	maybeAuth := make([]echo.MiddlewareFunc, 0)
 	if len(tokens) > 0 {
-		maybeAuth = append(maybeAuth, middlewares.MakeAuth("X-Indexer-API-Token", tokens))
+		middleware = append(middleware, middlewares.MakeAuth("X-Indexer-API-Token", tokens))
 	}
 
 	api := ServerImplementation{
@@ -40,7 +42,7 @@ func Serve(ctx context.Context, serveAddr string, db idb.IndexerDb, log *log.Log
 		db:                             db,
 	}
 
-	generated.RegisterHandlers(e, &api, maybeAuth...)
+	generated.RegisterHandlers(e, &api, middleware...)
 	common.RegisterHandlers(e, &api)
 
 	if ctx == nil {
diff --git a/cmd/algorand-indexer/daemon.go b/cmd/algorand-indexer/daemon.go
index e9a30e9..869648c 100644
--- a/cmd/algorand-indexer/daemon.go
+++ b/cmd/algorand-indexer/daemon.go
@@ -142,9 +142,9 @@ func (bih *blockImporterHandler) HandleBlock(block *types.EncodedBlockCert) {
 	if len(block.Block.Payset) == 0 {
 		// accounting won't have updated the round state, so we do it here
 		stateJsonStr, err := db.GetMetastate("state")
-		var state idb.ImportState
+		var state importer.ImportState
 		if err == nil && stateJsonStr != "" {
-			state, err = idb.ParseImportState(stateJsonStr)
+			state, err = importer.ParseImportState(stateJsonStr)
 			if err != nil {
 				fmt.Fprintf(os.Stderr, "error parsing import state, %v\n", err)
 				panic("error parsing import state in bih")
diff --git a/cmd/algorand-indexer/import.go b/cmd/algorand-indexer/import.go
index 8dfc801..db386ae 100644
--- a/cmd/algorand-indexer/import.go
+++ b/cmd/algorand-indexer/import.go
@@ -17,9 +17,9 @@ var importCmd = &cobra.Command{
 		db := globalIndexerDb(nil)
 
 		helper := importer.ImportHelper{
-			BlockFileLimit: blockFileLimit,
+			BlockFileLimit:  blockFileLimit,
 			GenesisJsonPath: genesisJsonPath,
-			NumRoundsLimit: numRoundsLimit,
+			NumRoundsLimit:  numRoundsLimit,
 		}
 
 		helper.Import(db, args)
@@ -28,8 +28,8 @@ var importCmd = &cobra.Command{
 
 var (
 	genesisJsonPath string
-	numRoundsLimit int
-	blockFileLimit int
+	numRoundsLimit  int
+	blockFileLimit  int
 )
 
 func init() {
diff --git a/config/config.go b/config/config.go
index e2283a1..6df2441 100644
--- a/config/config.go
+++ b/config/config.go
@@ -12,6 +12,7 @@ import (
 const EnvPrefix = "INDEXER"
 const FileType = "yaml"
 const FileName = "algorand-indexer"
+
 var ConfigPaths = [...]string{".", "$HOME", "$HOME/.algorand-indexer/", "$HOME/.config/algorand-indexer/", "/etc/algorand-indexer/"}
 
 // BindFlags glues the cobra and viper libraries together.
diff --git a/go.mod b/go.mod
index b2ed1b2..8432a02 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/spf13/cobra v0.0.5
 	github.com/spf13/pflag v1.0.3
 	github.com/spf13/viper v1.7.1
-	github.com/stretchr/testify v1.5.1
+	github.com/stretchr/testify v1.6.1
 	github.com/valyala/fasttemplate v1.2.0 // indirect
 	golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de // indirect
 	golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
diff --git a/go.sum b/go.sum
index 5e1ac51..044d957 100644
--- a/go.sum
+++ b/go.sum
@@ -85,6 +85,7 @@ github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGS
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -133,6 +134,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -235,6 +238,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -418,6 +423,8 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/idb/dummy.go b/idb/dummy.go
index 8d00860..4ea1fd3 100644
--- a/idb/dummy.go
+++ b/idb/dummy.go
@@ -11,7 +11,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/algorand/go-algorand-sdk/encoding/json"
 	atypes "github.com/algorand/go-algorand-sdk/types"
 
 	models "github.com/algorand/indexer/api/generated/v2"
@@ -102,6 +101,10 @@ func (db *dummyIndexerDb) Applications(ctx context.Context, filter *models.Searc
 	return nil
 }
 
+func (db *dummyIndexerDb) Health() (state Health, err error) {
+	return Health{}, nil
+}
+
 type IndexerFactory interface {
 	Name() string
 	Build(arg string, opts *IndexerDbOptions) (IndexerDb, error)
@@ -147,7 +150,6 @@ type TxnExtra struct {
 // TODO: cockroachdb impl
 type IndexerDb interface {
 	// The next few functions define the import interface, functions for loading data into the database. StartBlock() through Get/SetMetastate().
-
 	StartBlock() error
 	AddTransaction(round uint64, intra int, txtypeenum int, assetid uint64, txn types.SignedTxnWithAD, participation [][]byte) error
 	CommitBlock(round uint64, timestamp int64, rewardslevel uint64, headerbytes []byte) error
@@ -175,6 +177,8 @@ type IndexerDb interface {
 	Assets(ctx context.Context, filter AssetsQuery) <-chan AssetRow
 	AssetBalances(ctx context.Context, abq AssetBalanceQuery) <-chan AssetBalanceRow
 	Applications(ctx context.Context, filter *models.SearchForApplicationsParams) <-chan ApplicationRow
+
+	Health() (status Health, err error)
 }
 
 func GetAccount(idb IndexerDb, addr []byte) (account models.Account, err error) {
@@ -519,11 +523,9 @@ func b32np(data []byte) string {
 	return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(data)
 }
 
-type ImportState struct {
-	AccountRound int64 `codec:"account_round"`
-}
-
-func ParseImportState(js string) (istate ImportState, err error) {
-	err = json.Decode([]byte(js), &istate)
-	return
+type Health struct {
+	Data          *map[string]interface{} `json:"data,omitempty""`
+	Round         uint64                  `json:"round"`
+	IsMigrating   bool                    `json:"is-migrating"`
+	DbUnavailable bool                    `json:"db-unavailable"`
 }
diff --git a/idb/migration/migration.go b/idb/migration/migration.go
new file mode 100644
index 0000000..987f81f
--- /dev/null
+++ b/idb/migration/migration.go
@@ -0,0 +1,166 @@
+package migration
+
+import (
+	"errors"
+	"fmt"
+	"sync"
+)
+
+var DuplicateIDErr = errors.New("duplicate ID detected")
+var UnorderedIDErr = errors.New("migration IDs must be in ascending order")
+
+const StatusPending = "Migration pending"
+const StatusComplete = "Migrations Complete"
+const StatusActivePrefix = "Active migration: "
+const StatusErrorPrefix = "error during migration "
+
+// Handler is the function which will be executed to perform the migration for this task.
+type Handler func() error
+
+// Task is used to define a migration.
+type Task struct {
+	// MigrationId is an internal ID that can be used by IndexerDb implementations.
+	MigrationId int
+
+	// Handler is the function which will be executed to perform the migration for this task.
+	Handler Handler
+
+	// DBUnavailable indicates whether or not queries should be blocked until this task is executed. If there are
+	// multiple migrations the migration status should indicate that queries are blocked until all blocking tasks
+	// have completed.
+	DBUnavailable bool
+
+	// Description should be a human readable description of what the migration does.
+	Description string
+}
+
+// State is the current status of the migration.
+type State struct {
+	// Err is the last error which occurred during the migration. On an error the migration should halt.
+	Err      error
+
+	// Status is the most recent status message.
+	Status   string
+
+	// Running indicates whether or not a migration is currently running.
+	Running  bool
+
+	// Blocking indicates that one or more tasks have requested that the DB remain unavailable until they complete.
+	Blocking bool
+}
+
+// Migration manages the execution of multiple migration tasks and provides a mechanism for concurrent status checks.
+type Migration struct {
+	mutex      sync.RWMutex
+	tasks      []Task
+	blockUntil int
+	state      State
+}
+
+// Broken out to allow for testing.
+func (m *Migration) setTasks(migrationTasks []Task) error {
+	m.blockUntil = 0
+	ids := make(map[int]bool)
+	lastId := 0
+
+	for _, migration := range migrationTasks {
+		// migrations must be in ascending order
+		if lastId > migration.MigrationId {
+			return UnorderedIDErr
+		}
+		lastId = migration.MigrationId
+
+		// Prevent duplicate IDs
+		if ids[migration.MigrationId] {
+			return DuplicateIDErr
+		}
+		ids[migration.MigrationId] = true
+
+		// Make sure blockUntil is set to the last blocking migration
+		if migration.DBUnavailable {
+			m.blockUntil = migration.MigrationId
+		}
+	}
+
+	m.tasks = migrationTasks
+
+	return nil
+}
+
+// MakeMigration initializes
+func MakeMigration(migrationTasks []Task) (*Migration, error) {
+	m := &Migration{
+		tasks: migrationTasks,
+		state: State{
+			Err:      nil,
+			Status:   StatusPending,
+			Running:  false,
+			Blocking: true,
+		},
+	}
+
+	err := m.setTasks(migrationTasks)
+	return m, err
+}
+
+// GetStatus returns the current status of the migration. This function is thread safe.
+func (m *Migration) GetStatus() State {
+	m.mutex.RLock()
+	defer m.mutex.RUnlock()
+
+	return State{
+		Err:      m.state.Err,
+		Status:   m.state.Status,
+		Running:  m.state.Running,
+		Blocking: m.state.Blocking,
+	}
+}
+
+// update is a helper to set values in a thread safe way.
+func (m *Migration) update(err error, status string, running bool, blocking bool) {
+	m.mutex.Lock()
+
+	defer m.mutex.Unlock()
+
+	if err != m.state.Err {
+		m.state.Err = err
+	}
+
+	if status != m.state.Status {
+		m.state.Status = status
+	}
+
+	if running != m.state.Running {
+		m.state.Running = running
+	}
+
+	if blocking != m.state.Blocking {
+		m.state.Blocking = blocking
+	}
+}
+
+// RunMigrations runs all tasks which have been loaded into the migration. It will update the status accordingly as the
+// migration runs. This call will block execution until it completes and should be run in a go routine if that is not
+// expected.
+func (m *Migration) RunMigrations() {
+	blocking := true
+	for _, task := range m.tasks {
+		if task.MigrationId > m.blockUntil {
+			blocking = false
+		}
+
+		m.update(nil, StatusActivePrefix+task.Description, true, blocking)
+		err := task.Handler()
+
+		if err != nil {
+			err := fmt.Errorf("%s%d (%s): %v", StatusErrorPrefix, task.MigrationId, task.Description, err)
+			// If a migration failed, mark that the migration is blocking and terminate.
+			blocking = true
+			m.update(err, err.Error(), false, blocking)
+			return
+		}
+	}
+
+	m.update(nil, StatusComplete, false, false)
+	return
+}
diff --git a/idb/mocks/IndexerDb.go b/idb/mocks/IndexerDb.go
index 66d362e..d90ca35 100644
--- a/idb/mocks/IndexerDb.go
+++ b/idb/mocks/IndexerDb.go
@@ -229,6 +229,27 @@ func (_m *IndexerDb) GetProto(version string) (types.ConsensusParams, error) {
 	return r0, r1
 }
 
+// Health provides a mock function with given fields:
+func (_m *IndexerDb) Health() (idb.Health, error) {
+	ret := _m.Called()
+
+	var r0 idb.Health
+	if rf, ok := ret.Get(0).(func() idb.Health); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(idb.Health)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func() error); ok {
+		r1 = rf()
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // LoadGenesis provides a mock function with given fields: genesis
 func (_m *IndexerDb) LoadGenesis(genesis types.Genesis) error {
 	ret := _m.Called(genesis)
diff --git a/idb/postgres/mock_driver.go b/idb/postgres/mock_driver.go
new file mode 100644
index 0000000..f87b74a
--- /dev/null
+++ b/idb/postgres/mock_driver.go
@@ -0,0 +1,184 @@
+package postgres
+
+import (
+	"context"
+	"database/sql"
+	"database/sql/driver"
+	"errors"
+	"fmt"
+)
+
+// MakeMockDB initializes a sql.DB object with a mocked driver loaded
+// to respond with the provided statements.
+func MakeMockDB(statements []*MockStmt) *sql.DB {
+	driver := &MockDriver{
+		statementIdx: 0,
+		Statements:   statements,
+	}
+	return sql.OpenDB(driver)
+}
+
+// MockDriver is the core of a mocked sql.DB object. It implements
+// several interfaces: driver.Connector, driver.Conn,
+type MockDriver struct {
+	statementIdx int
+	Statements   []*MockStmt
+}
+
+// Connect - part of driver.Connector
+func (d *MockDriver) Connect(ctx context.Context) (driver.Conn, error) {
+	return d.Open("")
+}
+
+// Driver - part of driver.Connector
+func (d *MockDriver) Driver() driver.Driver {
+	return d
+}
+
+// Open - Part of sql.Driver
+func (d *MockDriver) Open(name string) (driver.Conn, error) {
+	//fmt.Println("driver.Driver : Open")
+	return d, nil
+}
+
+// Prepare - Part of driver.Conn
+func (d *MockDriver) Prepare(query string) (driver.Stmt, error) {
+	d.statementIdx++
+
+	if d.statementIdx > len(d.Statements) {
+		return nil, errors.New("not enough statements loaded into mock driver")
+	}
+
+	numParts := 0
+	for _, v := range query {
+		if string(v) == "$" {
+			numParts++
+		}
+	}
+
+	if numParts != d.Statements[d.statementIdx-1].parts {
+		return nil, fmt.Errorf("unexpected number of arguments in statement: %s", query)
+	}
+
+	return d.Statements[d.statementIdx-1], nil
+}
+
+// Close - Part of driver.Conn
+func (d *MockDriver) Close() error {
+	//fmt.Println("driver.Conn : Close")
+	return nil
+}
+
+// Begin - Part of driver.Conn
+func (d *MockDriver) Begin() (driver.Tx, error) {
+	//fmt.Println("driver.Conn : Begin")
+	return &MockTx{}, nil
+}
+
+// MakeMockStmt is used to orchestrate the lifecycle of a sql.DB statement.
+func MakeMockStmt(parts int, columns []string, rows [][]interface{}) *MockStmt {
+	return &MockStmt{
+		parts:   parts,
+		columns: columns,
+		rowNum:  0,
+		rows:    rows,
+	}
+}
+
+// MockStmt orchestrates the lifecycle of a sql.DB statement
+// It implements driver.Stmt and driver.Rows. If I needed them,
+// it probably would have also implemented driver.Tx and
+// driver.Result.
+type MockStmt struct {
+	// number of parameterized arguments
+	parts int
+
+	columns []string
+	rowNum  int
+	rows    [][]interface{}
+}
+
+// Close - Part of driver.Stmt interfaces
+func (s *MockStmt) Close() error {
+	//fmt.Println("driver.Stmt / driver.Rows - Close")
+	return nil
+}
+
+// NumInput - Part of driver.Stmt
+func (s *MockStmt) NumInput() int {
+	//fmt.Println("driver.Stmt - NumInput")
+	return s.parts
+}
+
+// Exec - Part of driver.Stmt
+func (s *MockStmt) Exec(args []driver.Value) (driver.Result, error) {
+	//fmt.Println("driver.Stmt - Exec")
+	return &MockResult{
+		insertId: 0,
+		rows:     0,
+	}, nil
+}
+
+// Query - Part of driver.Stmt
+func (s *MockStmt) Query(args []driver.Value) (driver.Rows, error) {
+	//fmt.Println("driver.Stmt - Query")
+	return s, nil
+}
+
+// Columns - Part of driver.Rows
+func (s *MockStmt) Columns() []string {
+	//fmt.Println("driver.Rows - Columns")
+	return s.columns
+}
+
+// Next - Part of driver.Rows
+func (s *MockStmt) Next(dest []driver.Value) error {
+	//fmt.Println("driver.Rows - Next")
+	if s.rowNum > len(s.rows) {
+		return errors.New("no more rows loaded in mock driver")
+	}
+
+	if len(dest) != len(s.rows[s.rowNum]) {
+		return errors.New("unexpected number of destination rows in mock driver")
+	}
+
+	// Copy mock values into the destination
+	for i, v := range s.rows[s.rowNum] {
+		dest[i] = v
+	}
+
+	s.rowNum++
+	return nil
+}
+
+// MockResult is something I didn't need but is part of the interface.
+type MockResult struct {
+	insertId int64
+	rows     int64
+}
+
+// LastInsertId - Part of driver.Result interface
+func (s *MockResult) LastInsertId() (int64, error) {
+	//fmt.Println("driver.Result - LastInsertId")
+	return s.insertId, nil
+}
+
+// RowsAffected - Part of driver.Result interface
+func (s *MockResult) RowsAffected() (int64, error) {
+	//fmt.Println("driver.Result - RowsAffected")
+	return s.rows, nil
+}
+
+// MockTx is something I didn't need but is part of the interface.
+type MockTx struct {
+}
+
+func (t *MockTx) Commit() error {
+	//fmt.Println("driver.Tx - Commit")
+	return nil
+}
+
+func (t *MockTx) Rollback() error {
+	//fmt.Println("driver.Tx - Rollback")
+	return nil
+}
diff --git a/idb/postgres/postgres.go b/idb/postgres/postgres.go
index 6748a05..bb092da 100644
--- a/idb/postgres/postgres.go
+++ b/idb/postgres/postgres.go
@@ -27,20 +27,37 @@ import (
 
 	models "github.com/algorand/indexer/api/generated/v2"
 	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/idb/migration"
+	"github.com/algorand/indexer/importer"
 	"github.com/algorand/indexer/types"
 )
 
 func OpenPostgres(connection string, opts *idb.IndexerDbOptions) (pdb *PostgresIndexerDb, err error) {
 	db, err := sql.Open("postgres", connection)
+
 	if err != nil {
 		return nil, err
 	}
+
+	if strings.Contains(connection, "readonly") {
+		if opts == nil {
+			opts = &idb.IndexerDbOptions{}
+		}
+		opts.ReadOnly = true
+	}
+
+	return openPostgres(db, opts)
+}
+
+// Allow tests to inject a DB
+func openPostgres(db *sql.DB, opts *idb.IndexerDbOptions) (pdb *PostgresIndexerDb, err error) {
 	pdb = &PostgresIndexerDb{
 		db:         db,
 		protoCache: make(map[string]types.ConsensusParams, 20),
 	}
+
 	// e.g. a user named "readonly" is in the connection string
-	readonly := ((opts != nil) && opts.ReadOnly) || strings.Contains(connection, "readonly")
+	readonly := (opts != nil) && opts.ReadOnly
 	if !readonly {
 		err = pdb.init()
 	}
@@ -59,12 +76,14 @@ type PostgresIndexerDb struct {
 	txprows [][]interface{}
 
 	protoCache map[string]types.ConsensusParams
+
+	migration *migration.Migration
 }
 
 func (db *PostgresIndexerDb) init() (err error) {
 	accountingStateJson, _ := db.GetMetastate("state")
 	hasAccounting := len(accountingStateJson) > 0
-	migrationStateJson, _ := db.GetMetastate("migration")
+	migrationStateJson, _ := db.GetMetastate(migrationMetastateKey)
 	hasMigration := len(migrationStateJson) > 0
 
 	if hasMigration || hasAccounting {
@@ -315,6 +334,7 @@ func (db *PostgresIndexerDb) SetMetastate(key, jsonStrValue string) (err error)
 }
 
 func (db *PostgresIndexerDb) GetMaxRound() (round uint64, err error) {
+	round = 0
 	row := db.db.QueryRow(`SELECT max(round) FROM block_header`)
 	err = row.Scan(&round)
 	return
@@ -1086,7 +1106,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 	if !any {
 		fmt.Printf("empty round %d\n", round)
 	}
-	var istate idb.ImportState
+	var istate importer.ImportState
 	staterow := tx.QueryRow(`SELECT v FROM metastate WHERE k = 'state'`)
 	var stateJsonStr string
 	err = staterow.Scan(&stateJsonStr)
@@ -2285,6 +2305,40 @@ func (db *PostgresIndexerDb) yieldApplicationsThread(ctx context.Context, rows *
 	close(out)
 }
 
+func (db *PostgresIndexerDb) Health() (health idb.Health, err error) {
+	var ptr *map[string]interface{}
+	migrating := false
+	blocking := false
+
+	// If we are not in read-only mode, there will be a migration object.
+	if db.migration != nil {
+		state := db.migration.GetStatus()
+
+		var data = make(map[string]interface{})
+		if state.Err != nil {
+			data["migration-error"] = state.Err.Error()
+		}
+		if state.Status != "" {
+			data["migration-status"] = state.Status
+		}
+
+		migrating = state.Running
+		blocking = state.Blocking
+
+		if len(data) > 0 {
+			ptr = &data
+		}
+	}
+
+	round, err := db.GetMaxRound()
+	return idb.Health{
+		Data:          ptr,
+		Round:         round,
+		IsMigrating:   migrating,
+		DbUnavailable: blocking,
+	}, err
+}
+
 type postgresFactory struct {
 }
 
diff --git a/idb/postgres/postgres_migrations.go b/idb/postgres/postgres_migrations.go
index 279373b..37d8560 100644
--- a/idb/postgres/postgres_migrations.go
+++ b/idb/postgres/postgres_migrations.go
@@ -16,6 +16,7 @@ import (
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
 
 	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/idb/migration"
 	"github.com/algorand/indexer/types"
 )
 
@@ -31,24 +32,30 @@ type MigrationState struct {
 }
 
 // A migration function should take care of writing back to metastate migration row
-type migrationFunc func(*PostgresIndexerDb, *MigrationState) error
+type postgresMigrationFunc func(*PostgresIndexerDb, *MigrationState) error
 
 type migrationStruct struct {
-	migrate migrationFunc
+	migrate postgresMigrationFunc
 
-	// The system should wait for this migration to finish before trying to import new data or serve data.
-	preventStartup bool
+	blocking bool
+
+	// Description of the migration
+	description string
 }
 
 var migrations []migrationStruct
 
-func anyBlockers(nextMigration int) bool {
-	for i := nextMigration; i < len(migrations); i++ {
-		if migrations[i].preventStartup {
-			return true
-		}
+type migrationTask struct {
+	migrationId    int
+	migration      migrationStruct
+	migrationState *MigrationState
+	abortChan      chan error
+}
+
+func wrapPostgresHandler(handler postgresMigrationFunc, db *PostgresIndexerDb, state *MigrationState) migration.Handler {
+	return func() error {
+		return handler(db, state)
 	}
-	return false
 }
 
 func (db *PostgresIndexerDb) runAvailableMigrations(migrationStateJson string) (err error) {
@@ -60,38 +67,37 @@ func (db *PostgresIndexerDb) runAvailableMigrations(migrationStateJson string) (
 		}
 	}
 
-	if state.NextMigration >= len(migrations) {
-		return nil
+	// Make migration tasks
+	nextMigration := state.NextMigration
+	tasks := make([]migration.Task, 0)
+	for nextMigration < len(migrations) {
+		tasks = append(tasks, migration.Task{
+			Handler:       wrapPostgresHandler(migrations[nextMigration].migrate, db, &state),
+			MigrationId:   nextMigration,
+			Description:   migrations[nextMigration].description,
+			DBUnavailable: migrations[nextMigration].blocking,
+		})
+		nextMigration++
 	}
-	if anyBlockers(state.NextMigration) {
-		// run migrations synchronously
-		log.Print("Indexer startup will be delayed until database migration completes")
-		return db.runMigrationState(&state, true)
-	} else {
-		log.Print("database migrations running in background...")
-		go db.runMigrationState(&state, false)
-		return nil
+
+	if len(tasks) > 0 {
+		// Add a task to mark migrations as done instead of using a channel.
+		tasks = append(tasks, migration.Task{
+			MigrationId: 9999999,
+			Handler: func() error {
+				return db.markMigrationsAsDone()
+			},
+			Description: "Mark migrations done",
+		})
 	}
-}
 
-func (db *PostgresIndexerDb) runMigrationState(state *MigrationState, blocking bool) (err error) {
-	nextMigration := state.NextMigration
-	for true {
-		err = migrations[nextMigration].migrate(db, state)
-		if err != nil {
-			return fmt.Errorf("error in migration %d: %v", nextMigration, err)
-		}
-		nextMigration++
-		if nextMigration >= len(migrations) {
-			return nil
-		}
-		state.NextMigration = nextMigration
-		if blocking && !anyBlockers(nextMigration) {
-			log.Print("database migrations will complete in background...")
-			go db.runMigrationState(state, false)
-			return nil
-		}
+	db.migration, err = migration.MakeMigration(tasks)
+	if err != nil {
+		return err
 	}
+
+	go db.migration.RunMigrations()
+
 	return nil
 }
 
@@ -152,7 +158,7 @@ func m3acfgFix(db *PostgresIndexerDb, state *MigrationState) (err error) {
 		assetIds = append(assetIds, aid)
 	}
 	rows.Close()
-	for true {
+	for {
 		nexti, err := m3acfgFixAsyncInner(db, state, assetIds)
 		if err != nil {
 			log.Printf("acfg fix chunk, %v", err)
@@ -288,11 +294,10 @@ func sqlMigration(db *PostgresIndexerDb, state *MigrationState, sqlLines []strin
 
 func init() {
 	migrations = []migrationStruct{
-		// {func, preventStartup}
-		{m0fixupTxid, false},
-		{m1fixupBlockTime, true}, // UPDATE over all blocks
-		{m2apps, true},           // schema change breaks other SQL
-		{m3acfgFix, false},
+		{m0fixupTxid, false, "Recompute the txid with corrected algorithm."},
+		{m1fixupBlockTime, true, "Adjust block time to UTC timezone."},
+		{m2apps, true, "Update DB Schema for Algorand application support."},
+		{m3acfgFix, false, "Recompute asset configurations with corrected merge function."},
 	}
 }
 
diff --git a/importer/helper.go b/importer/helper.go
index 2bf6281..2882b37 100644
--- a/importer/helper.go
+++ b/importer/helper.go
@@ -29,6 +29,15 @@ type ImportHelper struct {
 	BlockFileLimit int
 }
 
+type ImportState struct {
+	AccountRound int64 `codec:"account_round"`
+}
+
+func ParseImportState(js string) (istate ImportState, err error) {
+	err = json.Decode([]byte(js), &istate)
+	return
+}
+
 func (h *ImportHelper) Import(db idb.IndexerDb, args []string) {
 	err := ImportProto(db)
 	maybeFail(err, "import proto, %v", err)
@@ -199,7 +208,7 @@ func updateAccounting(db idb.IndexerDb, genesisJsonPath string, numRoundsLimit i
 	txnCount = 0
 	stateJsonStr, err := db.GetMetastate("state")
 	maybeFail(err, "getting import state, %v\n", err)
-	var state idb.ImportState
+	var state ImportState
 	if stateJsonStr == "" {
 		if genesisJsonPath != "" {
 			fmt.Printf("loading genesis %s\n", genesisJsonPath)
@@ -216,7 +225,7 @@ func updateAccounting(db idb.IndexerDb, genesisJsonPath string, numRoundsLimit i
 			return
 		}
 	} else {
-		state, err = idb.ParseImportState(stateJsonStr)
+		state, err = ParseImportState(stateJsonStr)
 		maybeFail(err, "parsing import state, %v\n", err)
 		fmt.Printf("will start from round >%d\n", state.AccountRound)
 	}
@@ -295,4 +304,3 @@ func (paths *blockTarPaths) Swap(i, j int) {
 	(*paths)[i] = (*paths)[j]
 	(*paths)[j] = t
 }
-


Test patch:
diff --git a/api/middlewares/migration_middleware_test.go b/api/middlewares/migration_middleware_test.go
new file mode 100644
index 0000000..983d6f7
--- /dev/null
+++ b/api/middlewares/migration_middleware_test.go
@@ -0,0 +1,50 @@
+package middlewares
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/labstack/echo/v4"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+
+	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/idb/mocks"
+)
+
+var errSuccess = errors.New("unexpected success")
+var e = echo.New()
+var testAPIHeader = "API-Header-Whatever"
+
+// success is the "next" handler, it is only called when a request is allowed to continue
+func success(ctx echo.Context) error {
+	return errSuccess
+}
+
+func TestMigrationMiddlewareWaiting(t *testing.T) {
+	mockIndexer := &mocks.IndexerDb{}
+
+	hMigrating := idb.Health{
+		IsMigrating: true,
+	}
+
+	mockIndexer.On("Health", mock.Anything, mock.Anything).Return(hMigrating, nil)
+	handler := MakeMigrationMiddleware(mockIndexer)(success)
+	err := handler(e.NewContext(nil, nil))
+
+	require.Error(t, err, InProgressError, `'IsMigrating' is true, so we should see an InProgressError`)
+}
+
+func TestMigrationMiddlewareDone(t *testing.T) {
+	mockIndexer := &mocks.IndexerDb{}
+
+	hReady := idb.Health{
+		IsMigrating: false,
+	}
+
+	mockIndexer.On("Health", mock.Anything, mock.Anything).Return(hReady, nil)
+	handler := MakeMigrationMiddleware(mockIndexer)(success)
+	err := handler(e.NewContext(nil, nil))
+
+	require.Error(t, err, errSuccess.Error(), `'IsMigrating' is false, so errSuccess should pass through`)
+}
diff --git a/idb/migration/migration_test.go b/idb/migration/migration_test.go
new file mode 100644
index 0000000..4d96be1
--- /dev/null
+++ b/idb/migration/migration_test.go
@@ -0,0 +1,332 @@
+package migration
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+var MigrationError = errors.New("Migration Error")
+var SlowSuccessTask1 = MakeTask(1*time.Second, nil, 1, false, "slow success")
+var SlowSuccessTask2 = MakeTask(1*time.Second, nil, 2, false, "slow success")
+var SlowSuccessTask3 = MakeTask(1*time.Second, nil, 3, false, "slow success")
+
+var FastSuccessTask1 = MakeTask(0*time.Second, nil, 1, false, "fast success")
+var FastSuccessTask2 = MakeTask(0*time.Second, nil, 2, false, "fast success")
+var FastSuccessTask3 = MakeTask(0*time.Second, nil, 3, false, "fast success")
+
+var FastErrorTask1 = MakeTask(0*time.Second, MigrationError, 1, false, "fast error")
+var FastErrorTask2 = MakeTask(0*time.Second, MigrationError, 2, false, "fast error")
+var FastErrorTask3 = MakeTask(0*time.Second, MigrationError, 3, false, "fast error")
+
+var SlowErrorTask1 = MakeTask(1*time.Second, MigrationError, 1, false, "slow error")
+var SlowErrorTask2 = MakeTask(1*time.Second, MigrationError, 2, false, "slow error")
+var SlowErrorTask3 = MakeTask(1*time.Second, MigrationError, 3, false, "slow error")
+
+var SlowSuccessBlockingTask1 = MakeTask(1*time.Second, nil, 1, true, "blocking slow success")
+var SlowSuccessBlockingTask2 = MakeTask(1*time.Second, nil, 2, true, "blocking slow success")
+var SlowSuccessBlockingTask3 = MakeTask(1*time.Second, nil, 3, true, "blocking slow success")
+
+var FastSuccessBlockingTask1 = MakeTask(0*time.Second, nil, 1, true, "blocking fast success")
+var FastSuccessBlockingTask2 = MakeTask(0*time.Second, nil, 2, true, "blocking fast success")
+var FastSuccessBlockingTask3 = MakeTask(0*time.Second, nil, 3, true, "blocking fast success")
+
+var FastErrorBlockingTask1 = MakeTask(0*time.Second, MigrationError, 1, true, "blocking fast error")
+var FastErrorBlockingTask2 = MakeTask(0*time.Second, MigrationError, 2, true, "blocking fast error")
+var FastErrorBlockingTask3 = MakeTask(0*time.Second, MigrationError, 3, true, "blocking fast error")
+
+var SlowErrorBlockingTask1 = MakeTask(1*time.Second, MigrationError, 1, true, "blocking slow error")
+var SlowErrorBlockingTask2 = MakeTask(1*time.Second, MigrationError, 2, true, "blocking slow error")
+var SlowErrorBlockingTask3 = MakeTask(1*time.Second, MigrationError, 3, true, "blocking slow error")
+
+type testTask struct {
+	id          int
+	description string
+	duration    time.Duration
+	blocking    bool
+	err         error
+}
+
+func MakeTask(d time.Duration, err error, id int, blocking bool, description string) testTask {
+	return testTask{
+		id:          id,
+		description: description,
+		duration:    d,
+		blocking:    blocking,
+		err:         err,
+	}
+}
+
+func (tt testTask) Get(migration *Migration, recorder *[]State) Task {
+	handler := func() error {
+		*recorder = append(*recorder, migration.GetStatus())
+
+		time.Sleep(tt.duration)
+		return tt.err
+	}
+
+	return Task{
+		MigrationId:   tt.id,
+		Handler:       handler,
+		Description:   tt.description,
+		DBUnavailable: tt.blocking,
+	}
+}
+
+func TestSuccessfulMigration(t *testing.T) {
+	testcases := []struct {
+		name       string
+		tasks      []testTask
+		startupErr string
+		errors     []string
+		statuses   []string
+		blocking   []bool
+		runTime    time.Duration
+	}{
+		{
+			name: "Duplicate ID error",
+			tasks: []testTask{
+				SlowSuccessTask1,
+				SlowSuccessTask1,
+			},
+			startupErr: DuplicateIDErr.Error(),
+			errors:     []string{},
+			statuses:   []string{StatusPending},
+			blocking:   []bool{true},
+			runTime:    1 * time.Second,
+		},
+		{
+			name: "Non-ascending ID error",
+			tasks: []testTask{
+				SlowSuccessTask2,
+				SlowSuccessTask1,
+			},
+			startupErr: UnorderedIDErr.Error(),
+			errors:     []string{},
+			statuses:   []string{StatusPending},
+			blocking:   []bool{true},
+			runTime:    1 * time.Second,
+		},
+		{
+			name:       "No handlers exit immediately.",
+			tasks:      []testTask{},
+			startupErr: "",
+			errors:     []string{},
+			statuses:   []string{StatusPending, StatusComplete},
+			blocking:   []bool{true, false},
+			runTime:    0 * time.Second,
+		},
+		{
+			name:       "One task",
+			tasks:      []testTask{SlowSuccessTask1},
+			startupErr: "",
+			errors:     []string{},
+			statuses:   []string{StatusPending, StatusActivePrefix + SlowSuccessTask1.description, StatusComplete},
+			blocking:   []bool{true, false, false},
+			runTime:    1 * time.Second,
+		},
+		{
+			name: "Two tasks",
+			tasks: []testTask{
+				SlowSuccessTask1,
+				SlowSuccessTask2,
+			},
+			startupErr: "",
+			errors:     []string{},
+			statuses: []string{
+				StatusPending,
+				StatusActivePrefix + SlowSuccessTask1.description,
+				StatusActivePrefix + SlowSuccessTask2.description,
+				StatusComplete},
+			blocking: []bool{true, false, false, false},
+			runTime:  2 * time.Second,
+		},
+		{
+			name: "3 Fast tasks",
+			tasks: []testTask{
+				FastSuccessTask1,
+				FastSuccessTask2,
+				FastSuccessTask3,
+			},
+			startupErr: "",
+			errors:     []string{},
+			statuses: []string{
+				StatusPending,
+				StatusActivePrefix + FastSuccessTask1.description,
+				StatusActivePrefix + FastSuccessTask2.description,
+				StatusActivePrefix + FastSuccessTask3.description,
+				StatusComplete},
+			blocking: []bool{true, false, false, false, false},
+			runTime:  0 * time.Second,
+		},
+		{
+			name: "Error right away",
+			tasks: []testTask{
+				FastErrorTask1,
+				SlowSuccessTask2,
+				SlowSuccessTask3,
+			},
+			startupErr: "",
+			errors:     []string{MigrationError.Error()},
+			statuses: []string{
+				StatusPending,
+				StatusActivePrefix + FastErrorTask1.description,
+				StatusErrorPrefix,
+			},
+			blocking: []bool{true, false, true},
+			runTime:  0 * time.Second,
+		},
+		{
+			name: "Error at the end of non blocking tasks",
+			tasks: []testTask{
+				SlowSuccessTask1,
+				SlowSuccessTask2,
+				SlowErrorTask3,
+			},
+			startupErr: "",
+			errors:     []string{MigrationError.Error()},
+			statuses: []string{
+				StatusPending,
+				StatusActivePrefix + SlowSuccessTask1.description,
+				StatusActivePrefix + SlowSuccessTask2.description,
+				StatusActivePrefix + SlowErrorTask3.description,
+				StatusErrorPrefix,
+			},
+			blocking: []bool{true, false, false, false, true},
+			runTime:  3 * time.Second,
+		},
+		{
+			name: "Only the first task is blocking",
+			tasks: []testTask{
+				SlowSuccessBlockingTask1,
+				SlowSuccessTask2,
+				SlowSuccessTask3,
+			},
+			startupErr: "",
+			errors:     []string{MigrationError.Error()},
+			statuses: []string{
+				StatusPending,
+				StatusActivePrefix + SlowSuccessBlockingTask1.description,
+				StatusActivePrefix + SlowSuccessTask2.description,
+				StatusActivePrefix + SlowSuccessTask3.description,
+				StatusComplete,
+			},
+			blocking: []bool{true, true, false, false, false},
+			runTime:  3 * time.Second,
+		},
+		{
+			name: "Only the middle task is blocking",
+			tasks: []testTask{
+				SlowSuccessTask1,
+				SlowSuccessBlockingTask2,
+				SlowSuccessTask3,
+			},
+			startupErr: "",
+			errors:     []string{MigrationError.Error()},
+			statuses: []string{
+				StatusPending,
+				StatusActivePrefix + SlowSuccessTask1.description,
+				StatusActivePrefix + SlowSuccessBlockingTask2.description,
+				StatusActivePrefix + SlowSuccessTask3.description,
+				StatusComplete,
+			},
+			blocking: []bool{true, true, true, false, false},
+			runTime:  3 * time.Second,
+		},
+		{
+			name: "Last task is blocking",
+			tasks: []testTask{
+				SlowSuccessTask1,
+				SlowSuccessTask2,
+				SlowSuccessBlockingTask3,
+			},
+			startupErr: "",
+			errors:     []string{MigrationError.Error()},
+			statuses: []string{
+				StatusPending,
+				StatusActivePrefix + SlowSuccessTask1.description,
+				StatusActivePrefix + SlowSuccessTask2.description,
+				StatusActivePrefix + SlowSuccessBlockingTask3.description,
+				StatusComplete,
+			},
+			blocking: []bool{true, true, true, true, false},
+			runTime:  3 * time.Second,
+		},
+	}
+
+	// Fail test at timeout
+	timeout := 10 * time.Second
+	// Allow the blocktime to be +/- fuzzyness
+	fuzzyness := 250 * time.Millisecond
+
+	for _, testcase := range testcases {
+		testcase := testcase
+		t.Run(testcase.name, func(t *testing.T) {
+			t.Parallel()
+
+			recorder := make([]State, 0)
+			tasks := make([]Task, 0)
+			migration, _ := MakeMigration(tasks)
+
+			for _, testTask := range testcase.tasks {
+				tasks = append(tasks, testTask.Get(migration, &recorder))
+			}
+
+			err := migration.setTasks(tasks)
+			if testcase.startupErr != "" {
+				require.EqualError(t, err, testcase.startupErr)
+				return
+			} else {
+				require.Equal(t, "", testcase.startupErr)
+			}
+
+			// Initial Status
+			recorder = append(recorder, migration.GetStatus())
+
+			start := time.Now()
+			migChan := make(chan struct{})
+			go func() {
+				migration.RunMigrations()
+				migChan <- struct{}{}
+			}()
+
+			select {
+			case <-migChan:
+				// Final Status
+				recorder = append(recorder, migration.GetStatus())
+				fmt.Println("Migration complete, check results...")
+			case <-time.After(timeout):
+				require.Fail(t, "Migration timeout")
+				return
+			}
+
+			// Verify expected run duration
+			end := time.Now()
+			runtime := end.Sub(start)
+			if testcase.runTime > runtime+fuzzyness || testcase.runTime < runtime-fuzzyness {
+				t.Fatalf("runtime outside nominal duration %s != %s", runtime, testcase.runTime)
+			}
+
+			// Check the statuses recorded during migration
+			errNum := 0
+			running := true
+			for idx, v := range recorder {
+				fmt.Println(v)
+				if v.Err != nil {
+					require.Contains(t, v.Err.Error(), testcase.errors[errNum])
+					errNum++
+				}
+				require.Contains(t, v.Status, testcase.statuses[idx])
+				require.Equal(t, testcase.blocking[idx], v.Blocking)
+
+				running = v.Running
+			}
+
+			// When the migration is complete, it better not be Running!
+			require.False(t, running)
+		})
+	}
+}
diff --git a/idb/postgres/postgres_test.go b/idb/postgres/postgres_test.go
new file mode 100644
index 0000000..f904258
--- /dev/null
+++ b/idb/postgres/postgres_test.go
@@ -0,0 +1,86 @@
+package postgres
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/algorand/indexer/idb"
+	"github.com/stretchr/testify/require"
+)
+
+func TestAllMigrations(t *testing.T) {
+	for idx, m := range migrations {
+		t.Run(fmt.Sprintf("Test migration %d", idx), func(t *testing.T) {
+			db := MakeMockDB([]*MockStmt{
+				// "state"
+				MakeMockStmt(
+					1,
+					[]string{"v"},
+					[][]interface{}{
+						{`{"account_round": 9000000}`},
+					}),
+				// "migration"
+				MakeMockStmt(
+					1,
+					[]string{"v"},
+					[][]interface{}{
+						{fmt.Sprintf(`{"next": %d}`, idx)},
+					}),
+			})
+
+			// This automatically runs migrations
+			pdb, err := openPostgres(db, &idb.IndexerDbOptions{
+				ReadOnly: false,
+			})
+			require.NoError(t, err)
+
+			// Just need a moment for the go routine to get started
+			time.Sleep(100 * time.Millisecond)
+
+			h, err := pdb.Health()
+			fmt.Printf("%v\n", h)
+			// Health attempts to get num rows...
+			require.Error(t, err, "not enough statements loaded into mock driver")
+
+			// There should be an error because I'm not attempting to mock the migration code.
+			//require.Contains(t, err.Error(), fmt.Sprintf("error during migration %d", idx))
+			str := fmt.Sprintf("error during migration %d (%s)", idx, m.description)
+			require.Contains(t, (*h.Data)["migration-error"], str)
+			require.Contains(t, (*h.Data)["migration-status"], str)
+		})
+	}
+}
+
+func TestNoMigrationsNeeded(t *testing.T) {
+	db := MakeMockDB([]*MockStmt{
+		// "state"
+		MakeMockStmt(
+			1,
+			[]string{"v"},
+			[][]interface{}{
+				{`{"account_round": 9000000}`},
+			}),
+		// "migration"
+		MakeMockStmt(
+			1,
+			[]string{"v"},
+			[][]interface{}{
+				{fmt.Sprintf(`{"next": %d}`, len(migrations)+1)},
+			}),
+	})
+
+	// This automatically runs migraions
+	pdb, err := openPostgres(db, &idb.IndexerDbOptions{
+		ReadOnly: false,
+	})
+
+	// Just need a moment for the go routine to get started
+	time.Sleep(100 * time.Millisecond)
+
+	h, err := pdb.Health()
+	// Health attempts to get num rows...
+	require.Error(t, err, "not enough statements loaded into mock driver")
+
+	require.Equal(t, (*h.Data)["migration-status"], "Migrations Complete")
+}