github.com/cosmos/cosmos-sdk@v0.50.10/docs/architecture/adr-038-state-listening.md (about) 1 # ADR 038: KVStore state listening 2 3 ## Changelog 4 5 * 11/23/2020: Initial draft 6 * 10/06/2022: Introduce plugin system based on hashicorp/go-plugin 7 * 10/14/2022: 8 * Add `ListenCommit`, flatten the state writes in a block to a single batch. 9 * Remove listeners from cache stores, should only listen to `rootmulti.Store`. 10 * Remove `HaltAppOnDeliveryError()`, the errors are propagated by default, the implementations should return nil if don't want to propogate errors. 11 * 26/05/2023: Update with ABCI 2.0 12 13 ## Status 14 15 Proposed 16 17 ## Abstract 18 19 This ADR defines a set of changes to enable listening to state changes of individual KVStores and exposing these data to consumers. 20 21 ## Context 22 23 Currently, KVStore data can be remotely accessed through [Queries](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules/messages-and-queries.md#queries) 24 which proceed either through Tendermint and the ABCI, or through the gRPC server. 25 In addition to these request/response queries, it would be beneficial to have a means of listening to state changes as they occur in real time. 26 27 ## Decision 28 29 We will modify the `CommitMultiStore` interface and its concrete (`rootmulti`) implementations and introduce a new `listenkv.Store` to allow listening to state changes in underlying KVStores. We don't need to listen to cache stores, because we can't be sure that the writes will be committed eventually, and the writes are duplicated in `rootmulti.Store` eventually, so we should only listen to `rootmulti.Store`. 30 We will introduce a plugin system for configuring and running streaming services that write these state changes and their surrounding ABCI message context to different destinations. 31 32 ### Listening 33 34 In a new file, `store/types/listening.go`, we will create a `MemoryListener` struct for streaming out protobuf encoded KV pairs state changes from a KVStore. 35 The `MemoryListener` will be used internally by the concrete `rootmulti` implementation to collect state changes from KVStores. 36 37 ```go 38 // MemoryListener listens to the state writes and accumulate the records in memory. 39 type MemoryListener struct { 40 stateCache []StoreKVPair 41 } 42 43 // NewMemoryListener creates a listener that accumulate the state writes in memory. 44 func NewMemoryListener() *MemoryListener { 45 return &MemoryListener{} 46 } 47 48 // OnWrite writes state change events to the internal cache 49 func (fl *MemoryListener) OnWrite(storeKey StoreKey, key []byte, value []byte, delete bool) { 50 fl.stateCache = append(fl.stateCache, StoreKVPair{ 51 StoreKey: storeKey.Name(), 52 Delete: delete, 53 Key: key, 54 Value: value, 55 }) 56 } 57 58 // PopStateCache returns the current state caches and set to nil 59 func (fl *MemoryListener) PopStateCache() []StoreKVPair { 60 res := fl.stateCache 61 fl.stateCache = nil 62 return res 63 } 64 ``` 65 66 We will also define a protobuf type for the KV pairs. In addition to the key and value fields this message 67 will include the StoreKey for the originating KVStore so that we can collect information from separate KVStores and determine the source of each KV pair. 68 69 ```protobuf 70 message StoreKVPair { 71 optional string store_key = 1; // the store key for the KVStore this pair originates from 72 required bool set = 2; // true indicates a set operation, false indicates a delete operation 73 required bytes key = 3; 74 required bytes value = 4; 75 } 76 ``` 77 78 ### ListenKVStore 79 80 We will create a new `Store` type `listenkv.Store` that the `rootmulti` store will use to wrap a `KVStore` to enable state listening. 81 We will configure the `Store` with a `MemoryListener` which will collect state changes for output to specific destinations. 82 83 ```go 84 // Store implements the KVStore interface with listening enabled. 85 // Operations are traced on each core KVStore call and written to any of the 86 // underlying listeners with the proper key and operation permissions 87 type Store struct { 88 parent types.KVStore 89 listener *types.MemoryListener 90 parentStoreKey types.StoreKey 91 } 92 93 // NewStore returns a reference to a new traceKVStore given a parent 94 // KVStore implementation and a buffered writer. 95 func NewStore(parent types.KVStore, psk types.StoreKey, listener *types.MemoryListener) *Store { 96 return &Store{parent: parent, listener: listener, parentStoreKey: psk} 97 } 98 99 // Set implements the KVStore interface. It traces a write operation and 100 // delegates the Set call to the parent KVStore. 101 func (s *Store) Set(key []byte, value []byte) { 102 types.AssertValidKey(key) 103 s.parent.Set(key, value) 104 s.listener.OnWrite(s.parentStoreKey, key, value, false) 105 } 106 107 // Delete implements the KVStore interface. It traces a write operation and 108 // delegates the Delete call to the parent KVStore. 109 func (s *Store) Delete(key []byte) { 110 s.parent.Delete(key) 111 s.listener.OnWrite(s.parentStoreKey, key, nil, true) 112 } 113 ``` 114 115 ### MultiStore interface updates 116 117 We will update the `CommitMultiStore` interface to allow us to wrap a `Memorylistener` to a specific `KVStore`. 118 Note that the `MemoryListener` will be attached internally by the concrete `rootmulti` implementation. 119 120 ```go 121 type CommitMultiStore interface { 122 ... 123 124 // AddListeners adds a listener for the KVStore belonging to the provided StoreKey 125 AddListeners(keys []StoreKey) 126 127 // PopStateCache returns the accumulated state change messages from MemoryListener 128 PopStateCache() []StoreKVPair 129 } 130 ``` 131 132 133 ### MultiStore implementation updates 134 135 We will adjust the `rootmulti` `GetKVStore` method to wrap the returned `KVStore` with a `listenkv.Store` if listening is turned on for that `Store`. 136 137 ```go 138 func (rs *Store) GetKVStore(key types.StoreKey) types.KVStore { 139 store := rs.stores[key].(types.KVStore) 140 141 if rs.TracingEnabled() { 142 store = tracekv.NewStore(store, rs.traceWriter, rs.traceContext) 143 } 144 if rs.ListeningEnabled(key) { 145 store = listenkv.NewStore(store, key, rs.listeners[key]) 146 } 147 148 return store 149 } 150 ``` 151 152 We will implement `AddListeners` to manage KVStore listeners internally and implement `PopStateCache` 153 for a means of retrieving the current state. 154 155 ```go 156 // AddListeners adds state change listener for a specific KVStore 157 func (rs *Store) AddListeners(keys []types.StoreKey) { 158 listener := types.NewMemoryListener() 159 for i := range keys { 160 rs.listeners[keys[i]] = listener 161 } 162 } 163 ``` 164 165 ```go 166 func (rs *Store) PopStateCache() []types.StoreKVPair { 167 var cache []types.StoreKVPair 168 for _, ls := range rs.listeners { 169 cache = append(cache, ls.PopStateCache()...) 170 } 171 sort.SliceStable(cache, func(i, j int) bool { 172 return cache[i].StoreKey < cache[j].StoreKey 173 }) 174 return cache 175 } 176 ``` 177 178 We will also adjust the `rootmulti` `CacheMultiStore` and `CacheMultiStoreWithVersion` methods to enable listening in 179 the cache layer. 180 181 ```go 182 func (rs *Store) CacheMultiStore() types.CacheMultiStore { 183 stores := make(map[types.StoreKey]types.CacheWrapper) 184 for k, v := range rs.stores { 185 store := v.(types.KVStore) 186 // Wire the listenkv.Store to allow listeners to observe the writes from the cache store, 187 // set same listeners on cache store will observe duplicated writes. 188 if rs.ListeningEnabled(k) { 189 store = listenkv.NewStore(store, k, rs.listeners[k]) 190 } 191 stores[k] = store 192 } 193 return cachemulti.NewStore(rs.db, stores, rs.keysByName, rs.traceWriter, rs.getTracingContext()) 194 } 195 ``` 196 197 ```go 198 func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStore, error) { 199 // ... 200 201 // Wire the listenkv.Store to allow listeners to observe the writes from the cache store, 202 // set same listeners on cache store will observe duplicated writes. 203 if rs.ListeningEnabled(key) { 204 cacheStore = listenkv.NewStore(cacheStore, key, rs.listeners[key]) 205 } 206 207 cachedStores[key] = cacheStore 208 } 209 210 return cachemulti.NewStore(rs.db, cachedStores, rs.keysByName, rs.traceWriter, rs.getTracingContext()), nil 211 } 212 ``` 213 214 ### Exposing the data 215 216 #### Streaming Service 217 218 We will introduce a new `ABCIListener` interface that plugs into the BaseApp and relays ABCI requests and responses 219 so that the service can group the state changes with the ABCI requests. 220 221 ```go 222 // baseapp/streaming.go 223 224 // ABCIListener is the interface that we're exposing as a streaming service. 225 type ABCIListener interface { 226 // ListenFinalizeBlock updates the streaming service with the latest FinalizeBlock messages 227 ListenFinalizeBlock(ctx context.Context, req abci.RequestFinalizeBlock, res abci.ResponseFinalizeBlock) error 228 // ListenCommit updates the steaming service with the latest Commit messages and state changes 229 ListenCommit(ctx context.Context, res abci.ResponseCommit, changeSet []*StoreKVPair) error 230 } 231 ``` 232 233 #### BaseApp Registration 234 235 We will add a new method to the `BaseApp` to enable the registration of `StreamingService`s: 236 237 ```go 238 // SetStreamingService is used to set a streaming service into the BaseApp hooks and load the listeners into the multistore 239 func (app *BaseApp) SetStreamingService(s ABCIListener) { 240 // register the StreamingService within the BaseApp 241 // BaseApp will pass BeginBlock, DeliverTx, and EndBlock requests and responses to the streaming services to update their ABCI context 242 app.abciListeners = append(app.abciListeners, s) 243 } 244 ``` 245 246 We will add two new fields to the `BaseApp` struct: 247 248 ```go 249 type BaseApp struct { 250 251 ... 252 253 // abciListenersAsync for determining if abciListeners will run asynchronously. 254 // When abciListenersAsync=false and stopNodeOnABCIListenerErr=false listeners will run synchronized but will not stop the node. 255 // When abciListenersAsync=true stopNodeOnABCIListenerErr will be ignored. 256 abciListenersAsync bool 257 258 // stopNodeOnABCIListenerErr halts the node when ABCI streaming service listening results in an error. 259 // stopNodeOnABCIListenerErr=true must be paired with abciListenersAsync=false. 260 stopNodeOnABCIListenerErr bool 261 } 262 ``` 263 264 #### ABCI Event Hooks 265 266 We will modify the `FinalizeBlock` and `Commit` methods to pass ABCI requests and responses 267 to any streaming service hooks registered with the `BaseApp`. 268 269 ```go 270 func (app *BaseApp) FinalizeBlock(req abci.RequestFinalizeBlock) abci.ResponseFinalizeBlock { 271 272 var abciRes abci.ResponseFinalizeBlock 273 defer func() { 274 // call the streaming service hook with the FinalizeBlock messages 275 for _, abciListener := range app.abciListeners { 276 ctx := app.finalizeState.ctx 277 blockHeight := ctx.BlockHeight() 278 if app.abciListenersAsync { 279 go func(req abci.RequestFinalizeBlock, res abci.ResponseFinalizeBlock) { 280 if err := app.abciListener.FinalizeBlock(blockHeight, req, res); err != nil { 281 app.logger.Error("FinalizeBlock listening hook failed", "height", blockHeight, "err", err) 282 } 283 }(req, abciRes) 284 } else { 285 if err := app.abciListener.ListenFinalizeBlock(blockHeight, req, res); err != nil { 286 app.logger.Error("FinalizeBlock listening hook failed", "height", blockHeight, "err", err) 287 if app.stopNodeOnABCIListenerErr { 288 os.Exit(1) 289 } 290 } 291 } 292 } 293 }() 294 295 ... 296 297 return abciRes 298 } 299 ``` 300 301 ```go 302 func (app *BaseApp) Commit() abci.ResponseCommit { 303 304 ... 305 306 res := abci.ResponseCommit{ 307 Data: commitID.Hash, 308 RetainHeight: retainHeight, 309 } 310 311 // call the streaming service hook with the Commit messages 312 for _, abciListener := range app.abciListeners { 313 ctx := app.deliverState.ctx 314 blockHeight := ctx.BlockHeight() 315 changeSet := app.cms.PopStateCache() 316 if app.abciListenersAsync { 317 go func(res abci.ResponseCommit, changeSet []store.StoreKVPair) { 318 if err := app.abciListener.ListenCommit(ctx, res, changeSet); err != nil { 319 app.logger.Error("ListenCommit listening hook failed", "height", blockHeight, "err", err) 320 } 321 }(res, changeSet) 322 } else { 323 if err := app.abciListener.ListenCommit(ctx, res, changeSet); err != nil { 324 app.logger.Error("ListenCommit listening hook failed", "height", blockHeight, "err", err) 325 if app.stopNodeOnABCIListenerErr { 326 os.Exit(1) 327 } 328 } 329 } 330 } 331 332 ... 333 334 return res 335 } 336 ``` 337 338 #### Go Plugin System 339 340 We propose a plugin architecture to load and run `Streaming` plugins and other types of implementations. We will introduce a plugin 341 system over gRPC that is used to load and run Cosmos-SDK plugins. The plugin system uses [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin). 342 Each plugin must have a struct that implements the `plugin.Plugin` interface and an `Impl` interface for processing messages over gRPC. 343 Each plugin must also have a message protocol defined for the gRPC service: 344 345 ```go 346 // streaming/plugins/abci/{plugin_version}/interface.go 347 348 // Handshake is a common handshake that is shared by streaming and host. 349 // This prevents users from executing bad plugins or executing a plugin 350 // directory. It is a UX feature, not a security feature. 351 var Handshake = plugin.HandshakeConfig{ 352 ProtocolVersion: 1, 353 MagicCookieKey: "ABCI_LISTENER_PLUGIN", 354 MagicCookieValue: "ef78114d-7bdf-411c-868f-347c99a78345", 355 } 356 357 // ListenerPlugin is the base struc for all kinds of go-plugin implementations 358 // It will be included in interfaces of different Plugins 359 type ABCIListenerPlugin struct { 360 // GRPCPlugin must still implement the Plugin interface 361 plugin.Plugin 362 // Concrete implementation, written in Go. This is only used for plugins 363 // that are written in Go. 364 Impl baseapp.ABCIListener 365 } 366 367 func (p *ListenerGRPCPlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error { 368 RegisterABCIListenerServiceServer(s, &GRPCServer{Impl: p.Impl}) 369 return nil 370 } 371 372 func (p *ListenerGRPCPlugin) GRPCClient( 373 _ context.Context, 374 _ *plugin.GRPCBroker, 375 c *grpc.ClientConn, 376 ) (interface{}, error) { 377 return &GRPCClient{client: NewABCIListenerServiceClient(c)}, nil 378 } 379 ``` 380 381 The `plugin.Plugin` interface has two methods `Client` and `Server`. For our GRPC service these are `GRPCClient` and `GRPCServer` 382 The `Impl` field holds the concrete implementation of our `baseapp.ABCIListener` interface written in Go. 383 Note: this is only used for plugin implementations written in Go. 384 385 The advantage of having such a plugin system is that within each plugin authors can define the message protocol in a way that fits their use case. 386 For example, when state change listening is desired, the `ABCIListener` message protocol can be defined as below (*for illustrative purposes only*). 387 When state change listening is not desired than `ListenCommit` can be omitted from the protocol. 388 389 ```protobuf 390 syntax = "proto3"; 391 392 ... 393 394 message Empty {} 395 396 message ListenFinalizeBlockRequest { 397 RequestFinalizeBlock req = 1; 398 ResponseFinalizeBlock res = 2; 399 } 400 message ListenCommitRequest { 401 int64 block_height = 1; 402 ResponseCommit res = 2; 403 repeated StoreKVPair changeSet = 3; 404 } 405 406 // plugin that listens to state changes 407 service ABCIListenerService { 408 rpc ListenFinalizeBlock(ListenFinalizeBlockRequest) returns (Empty); 409 rpc ListenCommit(ListenCommitRequest) returns (Empty); 410 } 411 ``` 412 413 ```protobuf 414 ... 415 // plugin that doesn't listen to state changes 416 service ABCIListenerService { 417 rpc ListenFinalizeBlock(ListenFinalizeBlockRequest) returns (Empty); 418 rpc ListenCommit(ListenCommitRequest) returns (Empty); 419 } 420 ``` 421 422 Implementing the service above: 423 424 ```go 425 // streaming/plugins/abci/{plugin_version}/grpc.go 426 427 var ( 428 _ baseapp.ABCIListener = (*GRPCClient)(nil) 429 ) 430 431 // GRPCClient is an implementation of the ABCIListener and ABCIListenerPlugin interfaces that talks over RPC. 432 type GRPCClient struct { 433 client ABCIListenerServiceClient 434 } 435 436 func (m *GRPCClient) ListenFinalizeBlock(goCtx context.Context, req abci.RequestFinalizeBlock, res abci.ResponseFinalizeBlock) error { 437 ctx := sdk.UnwrapSDKContext(goCtx) 438 _, err := m.client.ListenDeliverTx(ctx, &ListenDeliverTxRequest{BlockHeight: ctx.BlockHeight(), Req: req, Res: res}) 439 return err 440 } 441 442 func (m *GRPCClient) ListenCommit(goCtx context.Context, res abci.ResponseCommit, changeSet []store.StoreKVPair) error { 443 ctx := sdk.UnwrapSDKContext(goCtx) 444 _, err := m.client.ListenCommit(ctx, &ListenCommitRequest{BlockHeight: ctx.BlockHeight(), Res: res, ChangeSet: changeSet}) 445 return err 446 } 447 448 // GRPCServer is the gRPC server that GRPCClient talks to. 449 type GRPCServer struct { 450 // This is the real implementation 451 Impl baseapp.ABCIListener 452 } 453 454 func (m *GRPCServer) ListenFinalizeBlock(ctx context.Context, req *ListenFinalizeBlockRequest) (*Empty, error) { 455 return &Empty{}, m.Impl.ListenFinalizeBlock(ctx, req.Req, req.Res) 456 } 457 458 func (m *GRPCServer) ListenCommit(ctx context.Context, req *ListenCommitRequest) (*Empty, error) { 459 return &Empty{}, m.Impl.ListenCommit(ctx, req.Res, req.ChangeSet) 460 } 461 462 ``` 463 464 And the pre-compiled Go plugin `Impl`(*this is only used for plugins that are written in Go*): 465 466 ```go 467 // streaming/plugins/abci/{plugin_version}/impl/plugin.go 468 469 // Plugins are pre-compiled and loaded by the plugin system 470 471 // ABCIListener is the implementation of the baseapp.ABCIListener interface 472 type ABCIListener struct{} 473 474 func (m *ABCIListenerPlugin) ListenFinalizeBlock(ctx context.Context, req abci.RequestFinalizeBlock, res abci.ResponseFinalizeBlock) error { 475 // send data to external system 476 } 477 478 func (m *ABCIListenerPlugin) ListenCommit(ctx context.Context, res abci.ResponseCommit, changeSet []store.StoreKVPair) error { 479 // send data to external system 480 } 481 482 func main() { 483 plugin.Serve(&plugin.ServeConfig{ 484 HandshakeConfig: grpc_abci_v1.Handshake, 485 Plugins: map[string]plugin.Plugin{ 486 "grpc_plugin_v1": &grpc_abci_v1.ABCIListenerGRPCPlugin{Impl: &ABCIListenerPlugin{}}, 487 }, 488 489 // A non-nil value here enables gRPC serving for this streaming... 490 GRPCServer: plugin.DefaultGRPCServer, 491 }) 492 } 493 ``` 494 495 We will introduce a plugin loading system that will return `(interface{}, error)`. 496 This provides the advantage of using versioned plugins where the plugin interface and gRPC protocol change over time. 497 In addition, it allows for building independent plugin that can expose different parts of the system over gRPC. 498 499 ```go 500 func NewStreamingPlugin(name string, logLevel string) (interface{}, error) { 501 logger := hclog.New(&hclog.LoggerOptions{ 502 Output: hclog.DefaultOutput, 503 Level: toHclogLevel(logLevel), 504 Name: fmt.Sprintf("plugin.%s", name), 505 }) 506 507 // We're a host. Start by launching the streaming process. 508 env := os.Getenv(GetPluginEnvKey(name)) 509 client := plugin.NewClient(&plugin.ClientConfig{ 510 HandshakeConfig: HandshakeMap[name], 511 Plugins: PluginMap, 512 Cmd: exec.Command("sh", "-c", env), 513 Logger: logger, 514 AllowedProtocols: []plugin.Protocol{ 515 plugin.ProtocolNetRPC, plugin.ProtocolGRPC}, 516 }) 517 518 // Connect via RPC 519 rpcClient, err := client.Client() 520 if err != nil { 521 return nil, err 522 } 523 524 // Request streaming plugin 525 return rpcClient.Dispense(name) 526 } 527 528 ``` 529 530 We propose a `RegisterStreamingPlugin` function for the App to register `NewStreamingPlugin`s with the App's BaseApp. 531 Streaming plugins can be of `Any` type; therefore, the function takes in an interface vs a concrete type. 532 For example, we could have plugins of `ABCIListener`, `WasmListener` or `IBCListener`. Note that `RegisterStreamingPluing` function 533 is helper function and not a requirement. Plugin registration can easily be moved from the App to the BaseApp directly. 534 535 ```go 536 // baseapp/streaming.go 537 538 // RegisterStreamingPlugin registers streaming plugins with the App. 539 // This method returns an error if a plugin is not supported. 540 func RegisterStreamingPlugin( 541 bApp *BaseApp, 542 appOpts servertypes.AppOptions, 543 keys map[string]*types.KVStoreKey, 544 streamingPlugin interface{}, 545 ) error { 546 switch t := streamingPlugin.(type) { 547 case ABCIListener: 548 registerABCIListenerPlugin(bApp, appOpts, keys, t) 549 default: 550 return fmt.Errorf("unexpected plugin type %T", t) 551 } 552 return nil 553 } 554 ``` 555 556 ```go 557 func registerABCIListenerPlugin( 558 bApp *BaseApp, 559 appOpts servertypes.AppOptions, 560 keys map[string]*store.KVStoreKey, 561 abciListener ABCIListener, 562 ) { 563 asyncKey := fmt.Sprintf("%s.%s.%s", StreamingTomlKey, StreamingABCITomlKey, StreamingABCIAsync) 564 async := cast.ToBool(appOpts.Get(asyncKey)) 565 stopNodeOnErrKey := fmt.Sprintf("%s.%s.%s", StreamingTomlKey, StreamingABCITomlKey, StreamingABCIStopNodeOnErrTomlKey) 566 stopNodeOnErr := cast.ToBool(appOpts.Get(stopNodeOnErrKey)) 567 keysKey := fmt.Sprintf("%s.%s.%s", StreamingTomlKey, StreamingABCITomlKey, StreamingABCIKeysTomlKey) 568 exposeKeysStr := cast.ToStringSlice(appOpts.Get(keysKey)) 569 exposedKeys := exposeStoreKeysSorted(exposeKeysStr, keys) 570 bApp.cms.AddListeners(exposedKeys) 571 app.SetStreamingManager( 572 storetypes.StreamingManager{ 573 ABCIListeners: []storetypes.ABCIListener{abciListener}, 574 StopNodeOnErr: stopNodeOnErr, 575 }, 576 ) 577 } 578 ``` 579 580 ```go 581 func exposeAll(list []string) bool { 582 for _, ele := range list { 583 if ele == "*" { 584 return true 585 } 586 } 587 return false 588 } 589 590 func exposeStoreKeys(keysStr []string, keys map[string]*types.KVStoreKey) []types.StoreKey { 591 var exposeStoreKeys []types.StoreKey 592 if exposeAll(keysStr) { 593 exposeStoreKeys = make([]types.StoreKey, 0, len(keys)) 594 for _, storeKey := range keys { 595 exposeStoreKeys = append(exposeStoreKeys, storeKey) 596 } 597 } else { 598 exposeStoreKeys = make([]types.StoreKey, 0, len(keysStr)) 599 for _, keyStr := range keysStr { 600 if storeKey, ok := keys[keyStr]; ok { 601 exposeStoreKeys = append(exposeStoreKeys, storeKey) 602 } 603 } 604 } 605 // sort storeKeys for deterministic output 606 sort.SliceStable(exposeStoreKeys, func(i, j int) bool { 607 return exposeStoreKeys[i].Name() < exposeStoreKeys[j].Name() 608 }) 609 610 return exposeStoreKeys 611 } 612 ``` 613 614 The `NewStreamingPlugin` and `RegisterStreamingPlugin` functions are used to register a plugin with the App's BaseApp. 615 616 e.g. in `NewSimApp`: 617 618 ```go 619 func NewSimApp( 620 logger log.Logger, 621 db dbm.DB, 622 traceStore io.Writer, 623 loadLatest bool, 624 appOpts servertypes.AppOptions, 625 baseAppOptions ...func(*baseapp.BaseApp), 626 ) *SimApp { 627 628 ... 629 630 keys := sdk.NewKVStoreKeys( 631 authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, 632 minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, 633 govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey, 634 evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey, 635 ) 636 637 ... 638 639 // register streaming services 640 streamingCfg := cast.ToStringMap(appOpts.Get(baseapp.StreamingTomlKey)) 641 for service := range streamingCfg { 642 pluginKey := fmt.Sprintf("%s.%s.%s", baseapp.StreamingTomlKey, service, baseapp.StreamingPluginTomlKey) 643 pluginName := strings.TrimSpace(cast.ToString(appOpts.Get(pluginKey))) 644 if len(pluginName) > 0 { 645 logLevel := cast.ToString(appOpts.Get(flags.FlagLogLevel)) 646 plugin, err := streaming.NewStreamingPlugin(pluginName, logLevel) 647 if err != nil { 648 tmos.Exit(err.Error()) 649 } 650 if err := baseapp.RegisterStreamingPlugin(bApp, appOpts, keys, plugin); err != nil { 651 tmos.Exit(err.Error()) 652 } 653 } 654 } 655 656 return app 657 ``` 658 659 #### Configuration 660 661 The plugin system will be configured within an App's TOML configuration files. 662 663 ```toml 664 # gRPC streaming 665 [streaming] 666 667 # ABCI streaming service 668 [streaming.abci] 669 670 # The plugin version to use for ABCI listening 671 plugin = "abci_v1" 672 673 # List of kv store keys to listen to for state changes. 674 # Set to ["*"] to expose all keys. 675 keys = ["*"] 676 677 # Enable abciListeners to run asynchronously. 678 # When abciListenersAsync=false and stopNodeOnABCIListenerErr=false listeners will run synchronized but will not stop the node. 679 # When abciListenersAsync=true stopNodeOnABCIListenerErr will be ignored. 680 async = false 681 682 # Whether to stop the node on message deliver error. 683 stop-node-on-err = true 684 ``` 685 686 There will be four parameters for configuring `ABCIListener` plugin: `streaming.abci.plugin`, `streaming.abci.keys`, `streaming.abci.async` and `streaming.abci.stop-node-on-err`. 687 `streaming.abci.plugin` is the name of the plugin we want to use for streaming, `streaming.abci.keys` is a set of store keys for stores it listens to, 688 `streaming.abci.async` is bool enabling asynchronous listening and `streaming.abci.stop-node-on-err` is a bool that stops the node when true and when operating 689 on synchronized mode `streaming.abci.async=false`. Note that `streaming.abci.stop-node-on-err=true` will be ignored if `streaming.abci.async=true`. 690 691 The configuration above support additional streaming plugins by adding the plugin to the `[streaming]` configuration section 692 and registering the plugin with `RegisterStreamingPlugin` helper function. 693 694 Note the that each plugin must include `streaming.{service}.plugin` property as it is a requirement for doing the lookup and registration of the plugin 695 with the App. All other properties are unique to the individual services. 696 697 #### Encoding and decoding streams 698 699 ADR-038 introduces the interfaces and types for streaming state changes out from KVStores, associating this 700 data with their related ABCI requests and responses, and registering a service for consuming this data and streaming it to some destination in a final format. 701 Instead of prescribing a final data format in this ADR, it is left to a specific plugin implementation to define and document this format. 702 We take this approach because flexibility in the final format is necessary to support a wide range of streaming service plugins. For example, 703 the data format for a streaming service that writes the data out to a set of files will differ from the data format that is written to a Kafka topic. 704 705 ## Consequences 706 707 These changes will provide a means of subscribing to KVStore state changes in real time. 708 709 ### Backwards Compatibility 710 711 * This ADR changes the `CommitMultiStore` interface, implementations supporting the previous version of this interface will not support the new one 712 713 ### Positive 714 715 * Ability to listen to KVStore state changes in real time and expose these events to external consumers 716 717 ### Negative 718 719 * Changes `CommitMultiStore` interface and its implementations 720 721 ### Neutral 722 723 * Introduces additional- but optional- complexity to configuring and running a cosmos application 724 * If an application developer opts to use these features to expose data, they need to be aware of the ramifications/risks of that data exposure as it pertains to the specifics of their application