Simplify context (#4706)

Replace complex Context construct with a simpler immutible struct.
Only breaking change is not to support `Value` and `GetValue` as first class calls.
We do embed ctx.Context() as a raw context.Context instead to be used as you see fit.
This commit is contained in:
Ethan Frey 2019-07-16 15:40:42 +02:00 committed by Alessio Treglia
parent 6ca641891b
commit d3bb9f50e2
3 changed files with 193 additions and 286 deletions

View File

@ -0,0 +1,11 @@
4706 - Simplify context
Replace complex Context construct with a simpler immutible struct.
Only breaking change is not to support `Value` and `GetValue` as first class calls.
We do embed ctx.Context() as a raw context.Context instead to be used as you see fit.
Migration guide:
`ctx = ctx.WithValue(contextKeyBadProposal, false)` ->
`ctx = ctx.WithContext(context.WithValue(ctx.Context(), contextKeyBadProposal, false))`
A bit more verbose, but also allows `context.WithTimeout()`, etc and only used
in one function in this repo, in test code.

View File

@ -1,13 +1,10 @@
// nolint
package types
import (
"context"
"sync"
"time"
"github.com/golang/protobuf/proto"
"github.com/gogo/protobuf/proto"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
@ -16,172 +13,95 @@ import (
)
/*
The intent of Context is for it to be an immutable object that can be
cloned and updated cheaply with WithValue() and passed forward to the
next decorator or handler. For example,
Context is an immutable object contains all information needed to
process a request.
func MsgHandler(ctx Context, tx Tx) Result {
...
ctx = ctx.WithValue(key, value)
...
}
It contains a context.Context object inside if you want to use that,
but please do not over-use it. We try to keep all data structured
and standard additions here would be better just to add to the Context struct
*/
type Context struct {
context.Context
pst *thePast
gen int
// Don't add any other fields here,
// it's probably not what you want to do.
ctx context.Context
ms MultiStore
header abci.Header
chainID string
txBytes []byte
logger log.Logger
voteInfo []abci.VoteInfo
gasMeter GasMeter
blockGasMeter GasMeter
checkTx bool
minGasPrice DecCoins
consParams *abci.ConsensusParams
eventManager *EventManager
}
// Proposed rename, not done to avoid API breakage
type Request = Context
// Read-only accessors
func (c Context) Context() context.Context { return c.ctx }
func (c Context) MultiStore() MultiStore { return c.ms }
func (c Context) BlockHeight() int64 { return c.header.Height }
func (c Context) BlockTime() time.Time { return c.header.Time }
func (c Context) ChainID() string { return c.chainID }
func (c Context) TxBytes() []byte { return c.txBytes }
func (c Context) Logger() log.Logger { return c.logger }
func (c Context) VoteInfos() []abci.VoteInfo { return c.voteInfo }
func (c Context) GasMeter() GasMeter { return c.gasMeter }
func (c Context) BlockGasMeter() GasMeter { return c.blockGasMeter }
func (c Context) IsCheckTx() bool { return c.checkTx }
func (c Context) MinGasPrices() DecCoins { return c.minGasPrice }
func (c Context) EventManager() *EventManager { return c.eventManager }
// clone the header before returning
func (c Context) BlockHeader() abci.Header {
var msg = proto.Clone(&c.header).(*abci.Header)
return *msg
}
func (c Context) ConsensusParams() *abci.ConsensusParams {
return proto.Clone(c.consParams).(*abci.ConsensusParams)
}
// create a new context
func NewContext(ms MultiStore, header abci.Header, isCheckTx bool, logger log.Logger) Context {
c := Context{
Context: context.Background(),
pst: newThePast(),
gen: 0,
// https://github.com/gogo/protobuf/issues/519
header.Time = header.Time.UTC()
return Context{
ctx: context.Background(),
ms: ms,
header: header,
chainID: header.ChainID,
checkTx: isCheckTx,
logger: logger,
gasMeter: stypes.NewInfiniteGasMeter(),
minGasPrice: DecCoins{},
eventManager: NewEventManager(),
}
}
c = c.WithMultiStore(ms)
c = c.WithBlockHeader(header)
c = c.WithChainID(header.ChainID)
c = c.WithIsCheckTx(isCheckTx)
c = c.WithTxBytes(nil)
c = c.WithLogger(logger)
c = c.WithVoteInfos(nil)
c = c.WithGasMeter(stypes.NewInfiniteGasMeter())
c = c.WithMinGasPrices(DecCoins{})
c = c.WithConsensusParams(nil)
c = c.WithEventManager(NewEventManager())
func (c Context) WithContext(ctx context.Context) Context {
c.ctx = ctx
return c
}
// is context nil
func (c Context) IsZero() bool {
return c.Context == nil
}
// ----------------------------------------------------------------------------
// Getters
// ----------------------------------------------------------------------------
type contextKey int // local to the context module
const (
contextKeyMultiStore contextKey = iota
contextKeyBlockHeader
contextKeyChainID
contextKeyIsCheckTx
contextKeyTxBytes
contextKeyLogger
contextKeyVoteInfos
contextKeyGasMeter
contextKeyBlockGasMeter
contextKeyMinGasPrices
contextKeyConsensusParams
contextKeyEventManager
)
// context value for the provided key
func (c Context) Value(key interface{}) interface{} {
value := c.Context.Value(key)
if cloner, ok := value.(cloner); ok {
return cloner.Clone()
}
if message, ok := value.(proto.Message); ok {
return proto.Clone(message)
}
return value
}
func (c Context) MultiStore() MultiStore {
return c.Value(contextKeyMultiStore).(MultiStore)
}
func (c Context) BlockHeader() abci.Header { return c.Value(contextKeyBlockHeader).(abci.Header) }
func (c Context) BlockHeight() int64 { return c.BlockHeader().Height }
func (c Context) ChainID() string { return c.Value(contextKeyChainID).(string) }
func (c Context) TxBytes() []byte { return c.Value(contextKeyTxBytes).([]byte) }
func (c Context) Logger() log.Logger { return c.Value(contextKeyLogger).(log.Logger) }
func (c Context) VoteInfos() []abci.VoteInfo {
return c.Value(contextKeyVoteInfos).([]abci.VoteInfo)
}
func (c Context) GasMeter() GasMeter { return c.Value(contextKeyGasMeter).(GasMeter) }
func (c Context) BlockGasMeter() GasMeter { return c.Value(contextKeyBlockGasMeter).(GasMeter) }
func (c Context) IsCheckTx() bool { return c.Value(contextKeyIsCheckTx).(bool) }
func (c Context) MinGasPrices() DecCoins { return c.Value(contextKeyMinGasPrices).(DecCoins) }
func (c Context) ConsensusParams() *abci.ConsensusParams {
return c.Value(contextKeyConsensusParams).(*abci.ConsensusParams)
}
func (c Context) EventManager() *EventManager { return c.Value(contextKeyEventManager).(*EventManager) }
// ----------------------------------------------------------------------------
// Setters
// ----------------------------------------------------------------------------
func (c Context) WithValue(key interface{}, value interface{}) Context {
return c.withValue(key, value)
}
func (c Context) WithCloner(key interface{}, value cloner) Context {
return c.withValue(key, value)
}
func (c Context) WithCacheWrapper(key interface{}, value CacheWrapper) Context {
return c.withValue(key, value)
}
func (c Context) WithProtoMsg(key interface{}, value proto.Message) Context {
return c.withValue(key, value)
}
func (c Context) WithString(key interface{}, value string) Context {
return c.withValue(key, value)
}
func (c Context) WithInt32(key interface{}, value int32) Context {
return c.withValue(key, value)
}
func (c Context) WithUint32(key interface{}, value uint32) Context {
return c.withValue(key, value)
}
func (c Context) WithUint64(key interface{}, value uint64) Context {
return c.withValue(key, value)
}
func (c Context) withValue(key interface{}, value interface{}) Context {
c.pst.bump(Op{
gen: c.gen + 1,
key: key,
value: value,
}) // increment version for all relatives.
return Context{
Context: context.WithValue(c.Context, key, value),
pst: c.pst,
gen: c.gen + 1,
}
}
func (c Context) WithMultiStore(ms MultiStore) Context {
return c.withValue(contextKeyMultiStore, ms)
c.ms = ms
return c
}
func (c Context) WithBlockHeader(header abci.Header) Context {
var _ proto.Message = &header // for cloning.
return c.withValue(contextKeyBlockHeader, header)
// https://github.com/gogo/protobuf/issues/519
header.Time = header.Time.UTC()
c.header = header
return c
}
func (c Context) WithBlockTime(newTime time.Time) Context {
newHeader := c.BlockHeader()
newHeader.Time = newTime
// https://github.com/gogo/protobuf/issues/519
newHeader.Time = newTime.UTC()
return c.WithBlockHeader(newHeader)
}
@ -197,36 +117,78 @@ func (c Context) WithBlockHeight(height int64) Context {
return c.WithBlockHeader(newHeader)
}
func (c Context) WithChainID(chainID string) Context { return c.withValue(contextKeyChainID, chainID) }
func (c Context) WithTxBytes(txBytes []byte) Context { return c.withValue(contextKeyTxBytes, txBytes) }
func (c Context) WithLogger(logger log.Logger) Context { return c.withValue(contextKeyLogger, logger) }
func (c Context) WithVoteInfos(VoteInfos []abci.VoteInfo) Context {
return c.withValue(contextKeyVoteInfos, VoteInfos)
func (c Context) WithChainID(chainID string) Context {
c.chainID = chainID
return c
}
func (c Context) WithGasMeter(meter GasMeter) Context { return c.withValue(contextKeyGasMeter, meter) }
func (c Context) WithTxBytes(txBytes []byte) Context {
c.txBytes = txBytes
return c
}
func (c Context) WithLogger(logger log.Logger) Context {
c.logger = logger
return c
}
func (c Context) WithVoteInfos(voteInfo []abci.VoteInfo) Context {
c.voteInfo = voteInfo
return c
}
func (c Context) WithGasMeter(meter GasMeter) Context {
c.gasMeter = meter
return c
}
func (c Context) WithBlockGasMeter(meter GasMeter) Context {
return c.withValue(contextKeyBlockGasMeter, meter)
c.blockGasMeter = meter
return c
}
func (c Context) WithIsCheckTx(isCheckTx bool) Context {
return c.withValue(contextKeyIsCheckTx, isCheckTx)
c.checkTx = isCheckTx
return c
}
func (c Context) WithMinGasPrices(gasPrices DecCoins) Context {
return c.withValue(contextKeyMinGasPrices, gasPrices)
c.minGasPrice = gasPrices
return c
}
func (c Context) WithConsensusParams(params *abci.ConsensusParams) Context {
return c.withValue(contextKeyConsensusParams, params)
c.consParams = params
return c
}
func (c Context) WithEventManager(em *EventManager) Context {
return c.WithValue(contextKeyEventManager, em)
c.eventManager = em
return c
}
// TODO: remove???
func (c Context) IsZero() bool {
return c.ms == nil
}
// WithValue is deprecated, provided for backwards compatibility
// Please use
// ctx = ctx.WithContext(context.WithValue(ctx.Context(), key, false))
// instead of
// ctx = ctx.WithValue(key, false)
func (c Context) WithValue(key, value interface{}) Context {
c.ctx = context.WithValue(c.ctx, key, value)
return c
}
// Value is deprecated, provided for backwards compatibility
// Please use
// ctx.Context().Value(key)
// instead of
// ctx.Value(key)
func (c Context) Value(key interface{}) interface{} {
return c.ctx.Value(key)
}
// ----------------------------------------------------------------------------
@ -250,65 +212,3 @@ func (c Context) CacheContext() (cc Context, writeCache func()) {
cc = c.WithMultiStore(cms)
return cc, cms.Write
}
//----------------------------------------
// thePast
// Returns false if ver <= 0 || ver > len(c.pst.ops).
// The first operation is version 1.
func (c Context) GetOp(ver int64) (Op, bool) {
return c.pst.getOp(ver)
}
//----------------------------------------
// Misc.
type cloner interface {
Clone() interface{} // deep copy
}
// TODO add description
type Op struct {
// type is always 'with'
gen int
key interface{}
value interface{}
}
type thePast struct {
mtx sync.RWMutex
ver int
ops []Op
}
func newThePast() *thePast {
return &thePast{
ver: 0,
ops: nil,
}
}
func (pst *thePast) bump(op Op) {
pst.mtx.Lock()
pst.ver++
pst.ops = append(pst.ops, op)
pst.mtx.Unlock()
}
func (pst *thePast) version() int {
pst.mtx.RLock()
defer pst.mtx.RUnlock()
return pst.ver
}
// Returns false if ver <= 0 || ver > len(pst.ops).
// The first operation is version 1.
func (pst *thePast) getOp(ver int64) (Op, bool) {
pst.mtx.RLock()
defer pst.mtx.RUnlock()
l := int64(len(pst.ops))
if l < ver || ver <= 0 {
return Op{}, false
}
return pst.ops[ver-1], true
}

View File

@ -44,18 +44,6 @@ func (l MockLogger) With(kvs ...interface{}) log.Logger {
panic("not implemented")
}
func TestContextGetOpShouldNeverPanic(t *testing.T) {
var ms types.MultiStore
ctx := types.NewContext(ms, abci.Header{}, false, log.NewNopLogger())
indices := []int64{
-10, 1, 0, 10, 20,
}
for _, index := range indices {
_, _ = ctx.GetOp(index)
}
}
func defaultContext(key types.StoreKey) types.Context {
db := dbm.NewMemDB()
cms := store.NewCommitMultiStore(db)
@ -109,55 +97,11 @@ func (d dummy) Clone() interface{} {
return d
}
// Testing saving/loading primitive values to/from the context
func TestContextWithPrimitive(t *testing.T) {
ctx := types.NewContext(nil, abci.Header{}, false, log.NewNopLogger())
clonerkey := "cloner"
stringkey := "string"
int32key := "int32"
uint32key := "uint32"
uint64key := "uint64"
keys := []string{clonerkey, stringkey, int32key, uint32key, uint64key}
for _, key := range keys {
require.Nil(t, ctx.Value(key))
}
clonerval := dummy(1)
stringval := "string"
int32val := int32(1)
uint32val := uint32(2)
uint64val := uint64(3)
ctx = ctx.
WithCloner(clonerkey, clonerval).
WithString(stringkey, stringval).
WithInt32(int32key, int32val).
WithUint32(uint32key, uint32val).
WithUint64(uint64key, uint64val)
require.Equal(t, clonerval, ctx.Value(clonerkey))
require.Equal(t, stringval, ctx.Value(stringkey))
require.Equal(t, int32val, ctx.Value(int32key))
require.Equal(t, uint32val, ctx.Value(uint32key))
require.Equal(t, uint64val, ctx.Value(uint64key))
}
// Testing saving/loading sdk type values to/from the context
func TestContextWithCustom(t *testing.T) {
var ctx types.Context
require.True(t, ctx.IsZero())
require.Panics(t, func() { ctx.BlockHeader() })
require.Panics(t, func() { ctx.BlockHeight() })
require.Panics(t, func() { ctx.ChainID() })
require.Panics(t, func() { ctx.TxBytes() })
require.Panics(t, func() { ctx.Logger() })
require.Panics(t, func() { ctx.VoteInfos() })
require.Panics(t, func() { ctx.GasMeter() })
header := abci.Header{}
height := int64(1)
chainid := "chainid"
@ -192,9 +136,6 @@ func TestContextWithCustom(t *testing.T) {
func TestContextHeader(t *testing.T) {
var ctx types.Context
require.Panics(t, func() { ctx.BlockHeader() })
require.Panics(t, func() { ctx.BlockHeight() })
height := int64(5)
time := time.Now()
addr := secp256k1.GenPrivKey().PubKey().Address()
@ -208,6 +149,61 @@ func TestContextHeader(t *testing.T) {
WithProposer(proposer)
require.Equal(t, height, ctx.BlockHeight())
require.Equal(t, height, ctx.BlockHeader().Height)
require.Equal(t, time, ctx.BlockHeader().Time)
require.Equal(t, time.UTC(), ctx.BlockHeader().Time)
require.Equal(t, proposer.Bytes(), ctx.BlockHeader().ProposerAddress)
}
func TestContextHeaderClone(t *testing.T) {
cases := map[string]struct {
h abci.Header
}{
"empty": {
h: abci.Header{},
},
"height": {
h: abci.Header{
Height: 77,
},
},
"time": {
h: abci.Header{
Time: time.Unix(12345677, 12345),
},
},
"zero time": {
h: abci.Header{
Time: time.Unix(0, 0),
},
},
"many items": {
h: abci.Header{
Height: 823,
Time: time.Unix(9999999999, 0),
ChainID: "silly-demo",
},
},
"many items with hash": {
h: abci.Header{
Height: 823,
Time: time.Unix(9999999999, 0),
ChainID: "silly-demo",
AppHash: []byte{5, 34, 11, 3, 23},
ConsensusHash: []byte{11, 3, 23, 87, 3, 1},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ctx := types.NewContext(nil, tc.h, false, nil)
require.Equal(t, tc.h.Height, ctx.BlockHeight())
require.Equal(t, tc.h.Time.UTC(), ctx.BlockTime())
// update only changes one field
var newHeight int64 = 17
ctx = ctx.WithBlockHeight(newHeight)
require.Equal(t, newHeight, ctx.BlockHeight())
require.Equal(t, tc.h.Time.UTC(), ctx.BlockTime())
})
}
}