Commit f7328c5e authored by Bas van Kervel's avatar Bas van Kervel

rpc: add pub/sub support

parent fb578f45
......@@ -28,6 +28,8 @@ import (
"sync"
"time"
"golang.org/x/net/context"
"github.com/ethereum/ethash"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
......@@ -457,16 +459,46 @@ func (s *PrivateAccountAPI) LockAccount(addr common.Address) bool {
// It offers only methods that operate on public data that is freely available to anyone.
type PublicBlockChainAPI struct {
config *core.ChainConfig
bc *core.BlockChain
chainDb ethdb.Database
eventMux *event.TypeMux
am *accounts.Manager
miner *miner.Miner
bc *core.BlockChain
chainDb ethdb.Database
eventMux *event.TypeMux
muNewBlockSubscriptions sync.Mutex // protects newBlocksSubscriptions
newBlockSubscriptions map[string]func(core.ChainEvent) error // callbacks for new block subscriptions
am *accounts.Manager
miner *miner.Miner
}
// NewPublicBlockChainAPI creates a new Etheruem blockchain API.
func NewPublicBlockChainAPI(config *core.ChainConfig, bc *core.BlockChain, m *miner.Miner, chainDb ethdb.Database, eventMux *event.TypeMux, am *accounts.Manager) *PublicBlockChainAPI {
return &PublicBlockChainAPI{config: config, bc: bc, miner: m, chainDb: chainDb, eventMux: eventMux, am: am}
api := &PublicBlockChainAPI{
config: config,
bc: bc,
miner: m,
chainDb: chainDb,
eventMux: eventMux,
am: am,
newBlockSubscriptions: make(map[string]func(core.ChainEvent) error),
}
go api.subscriptionLoop()
return api
}
// subscriptionLoop reads events from the global event mux and creates notifications for the matched subscriptions.
func (s *PublicBlockChainAPI) subscriptionLoop() {
sub := s.eventMux.Subscribe(core.ChainEvent{})
for event := range sub.Chan() {
if chainEvent, ok := event.Data.(core.ChainEvent); ok {
s.muNewBlockSubscriptions.Lock()
for id, notifyOf := range s.newBlockSubscriptions {
if notifyOf(chainEvent) == rpc.ErrNotificationNotFound {
delete(s.newBlockSubscriptions, id)
}
}
s.muNewBlockSubscriptions.Unlock()
}
}
}
// BlockNumber returns the block number of the chain head.
......@@ -564,20 +596,36 @@ type NewBlocksArgs struct {
// NewBlocks triggers a new block event each time a block is appended to the chain. It accepts an argument which allows
// the caller to specify whether the output should contain transactions and in what format.
func (s *PublicBlockChainAPI) NewBlocks(args NewBlocksArgs) (rpc.Subscription, error) {
sub := s.eventMux.Subscribe(core.ChainEvent{})
func (s *PublicBlockChainAPI) NewBlocks(ctx context.Context, args NewBlocksArgs) (rpc.Subscription, error) {
notifier, supported := ctx.Value(rpc.NotifierContextKey).(rpc.Notifier)
if !supported {
return nil, rpc.ErrNotificationsUnsupported
}
output := func(rawBlock interface{}) interface{} {
if event, ok := rawBlock.(core.ChainEvent); ok {
notification, err := s.rpcOutputBlock(event.Block, args.IncludeTransactions, args.TransactionDetails)
if err == nil {
return notification
}
// create a subscription that will remove itself when unsubscribed/cancelled
subscription, err := notifier.NewSubscription(func(subId string) {
s.muNewBlockSubscriptions.Lock()
delete(s.newBlockSubscriptions, subId)
s.muNewBlockSubscriptions.Unlock()
})
if err != nil {
return nil, err
}
// add a callback that is called on chain events which will format the block and notify the client
s.muNewBlockSubscriptions.Lock()
s.newBlockSubscriptions[subscription.ID()] = func(e core.ChainEvent) error {
if notification, err := s.rpcOutputBlock(e.Block, args.IncludeTransactions, args.TransactionDetails); err == nil {
return subscription.Notify(notification)
} else {
glog.V(logger.Warn).Info("unable to format block %v\n", err)
}
return rawBlock
return nil
}
s.muNewBlockSubscriptions.Unlock()
return rpc.NewSubscriptionWithOutputFormat(sub, output), nil
return subscription, nil
}
// GetCode returns the code stored at the given address in the state for the given block number.
......@@ -821,26 +869,75 @@ func newRPCTransaction(b *types.Block, txHash common.Hash) (*RPCTransaction, err
// PublicTransactionPoolAPI exposes methods for the RPC interface
type PublicTransactionPoolAPI struct {
eventMux *event.TypeMux
chainDb ethdb.Database
gpo *GasPriceOracle
bc *core.BlockChain
miner *miner.Miner
am *accounts.Manager
txPool *core.TxPool
txMu sync.Mutex
eventMux *event.TypeMux
chainDb ethdb.Database
gpo *GasPriceOracle
bc *core.BlockChain
miner *miner.Miner
am *accounts.Manager
txPool *core.TxPool
txMu sync.Mutex
muPendingTxSubs sync.Mutex
pendingTxSubs map[string]rpc.Subscription
}
// NewPublicTransactionPoolAPI creates a new RPC service with methods specific for the transaction pool.
func NewPublicTransactionPoolAPI(e *Ethereum) *PublicTransactionPoolAPI {
return &PublicTransactionPoolAPI{
eventMux: e.EventMux(),
gpo: NewGasPriceOracle(e),
chainDb: e.ChainDb(),
bc: e.BlockChain(),
am: e.AccountManager(),
txPool: e.TxPool(),
miner: e.Miner(),
api := &PublicTransactionPoolAPI{
eventMux: e.EventMux(),
gpo: NewGasPriceOracle(e),
chainDb: e.ChainDb(),
bc: e.BlockChain(),
am: e.AccountManager(),
txPool: e.TxPool(),
miner: e.Miner(),
pendingTxSubs: make(map[string]rpc.Subscription),
}
go api.subscriptionLoop()
return api
}
// subscriptionLoop listens for events on the global event mux and creates notifications for subscriptions.
func (s *PublicTransactionPoolAPI) subscriptionLoop() {
sub := s.eventMux.Subscribe(core.TxPreEvent{})
accountTimeout := time.NewTicker(10 * time.Second)
// only publish pending tx signed by one of the accounts in the node
accountSet := set.New()
accounts, _ := s.am.Accounts()
for _, acc := range accounts {
accountSet.Add(acc.Address)
}
for {
select {
case event := <-sub.Chan():
if event == nil {
continue
}
tx := event.Data.(core.TxPreEvent)
if from, err := tx.Tx.FromFrontier(); err == nil {
if accountSet.Has(from) {
s.muPendingTxSubs.Lock()
for id, sub := range s.pendingTxSubs {
if sub.Notify(tx.Tx.Hash()) == rpc.ErrNotificationNotFound {
delete(s.pendingTxSubs, id)
}
}
s.muPendingTxSubs.Unlock()
}
}
case <-accountTimeout.C:
// refresh account list when accounts are added/removed from the node.
if accounts, err := s.am.Accounts(); err == nil {
accountSet.Clear()
for _, acc := range accounts {
accountSet.Add(acc.Address)
}
}
}
}
}
......@@ -1275,40 +1372,27 @@ func (s *PublicTransactionPoolAPI) PendingTransactions() ([]*RPCTransaction, err
// NewPendingTransaction creates a subscription that is triggered each time a transaction enters the transaction pool
// and is send from one of the transactions this nodes manages.
func (s *PublicTransactionPoolAPI) NewPendingTransactions() (rpc.Subscription, error) {
sub := s.eventMux.Subscribe(core.TxPreEvent{})
accounts, err := s.am.Accounts()
if err != nil {
return rpc.Subscription{}, err
func (s *PublicTransactionPoolAPI) NewPendingTransactions(ctx context.Context) (rpc.Subscription, error) {
notifier, supported := ctx.Value(rpc.NotifierContextKey).(rpc.Notifier)
if !supported {
return nil, rpc.ErrNotificationsUnsupported
}
accountSet := set.New()
for _, account := range accounts {
accountSet.Add(account.Address)
}
accountSetLastUpdates := time.Now()
output := func(transaction interface{}) interface{} {
if time.Since(accountSetLastUpdates) > (time.Duration(2) * time.Second) {
if accounts, err = s.am.Accounts(); err != nil {
accountSet.Clear()
for _, account := range accounts {
accountSet.Add(account.Address)
}
accountSetLastUpdates = time.Now()
}
}
subscription, err := notifier.NewSubscription(func(id string) {
s.muPendingTxSubs.Lock()
delete(s.pendingTxSubs, id)
s.muPendingTxSubs.Unlock()
})
tx := transaction.(core.TxPreEvent)
if from, err := tx.Tx.FromFrontier(); err == nil {
if accountSet.Has(from) {
return tx.Tx.Hash()
}
}
return nil
if err != nil {
return nil, err
}
return rpc.NewSubscriptionWithOutputFormat(sub, output), nil
s.muPendingTxSubs.Lock()
s.pendingTxSubs[subscription.ID()] = subscription
s.muPendingTxSubs.Unlock()
return subscription, nil
}
// Resend accepts an existing transaction and a new gas price and limit. It will remove the given transaction from the
......
......@@ -310,7 +310,7 @@ func (s *Ethereum) APIs() []rpc.API {
}, {
Namespace: "eth",
Version: "1.0",
Service: downloader.NewPublicDownloaderAPI(s.Downloader()),
Service: downloader.NewPublicDownloaderAPI(s.Downloader(), s.EventMux()),
Public: true,
}, {
Namespace: "miner",
......
......@@ -17,18 +17,55 @@
package downloader
import (
"sync"
"golang.org/x/net/context"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/rpc"
)
// PublicDownloaderAPI provides an API which gives information about the current synchronisation status.
// It offers only methods that operates on data that can be available to anyone without security risks.
type PublicDownloaderAPI struct {
d *Downloader
d *Downloader
mux *event.TypeMux
muSyncSubscriptions sync.Mutex
syncSubscriptions map[string]rpc.Subscription
}
// NewPublicDownloaderAPI create a new PublicDownloaderAPI.
func NewPublicDownloaderAPI(d *Downloader) *PublicDownloaderAPI {
return &PublicDownloaderAPI{d}
func NewPublicDownloaderAPI(d *Downloader, m *event.TypeMux) *PublicDownloaderAPI {
api := &PublicDownloaderAPI{d: d, mux: m, syncSubscriptions: make(map[string]rpc.Subscription)}
go api.run()
return api
}
func (api *PublicDownloaderAPI) run() {
sub := api.mux.Subscribe(StartEvent{}, DoneEvent{}, FailedEvent{})
for event := range sub.Chan() {
var notification interface{}
switch event.Data.(type) {
case StartEvent:
result := &SyncingResult{Syncing: true}
result.Status.Origin, result.Status.Current, result.Status.Height, result.Status.Pulled, result.Status.Known = api.d.Progress()
notification = result
case DoneEvent, FailedEvent:
notification = false
}
api.muSyncSubscriptions.Lock()
for id, sub := range api.syncSubscriptions {
if sub.Notify(notification) == rpc.ErrNotificationNotFound {
delete(api.syncSubscriptions, id)
}
}
api.muSyncSubscriptions.Unlock()
}
}
// Progress gives progress indications when the node is synchronising with the Ethereum network.
......@@ -47,19 +84,25 @@ type SyncingResult struct {
}
// Syncing provides information when this nodes starts synchronising with the Ethereum network and when it's finished.
func (s *PublicDownloaderAPI) Syncing() (rpc.Subscription, error) {
sub := s.d.mux.Subscribe(StartEvent{}, DoneEvent{}, FailedEvent{})
func (api *PublicDownloaderAPI) Syncing(ctx context.Context) (rpc.Subscription, error) {
notifier, supported := ctx.Value(rpc.NotifierContextKey).(rpc.Notifier)
if !supported {
return nil, rpc.ErrNotificationsUnsupported
}
output := func(event interface{}) interface{} {
switch event.(type) {
case StartEvent:
result := &SyncingResult{Syncing: true}
result.Status.Origin, result.Status.Current, result.Status.Height, result.Status.Pulled, result.Status.Known = s.d.Progress()
return result
case DoneEvent, FailedEvent:
return false
}
return nil
subscription, err := notifier.NewSubscription(func(id string) {
api.muSyncSubscriptions.Lock()
delete(api.syncSubscriptions, id)
api.muSyncSubscriptions.Unlock()
})
if err != nil {
return nil, err
}
return rpc.NewSubscriptionWithOutputFormat(sub, output), nil
api.muSyncSubscriptions.Lock()
api.syncSubscriptions[subscription.ID()] = subscription
api.muSyncSubscriptions.Unlock()
return subscription, nil
}
......@@ -17,15 +17,13 @@
package filters
import (
"sync"
"time"
"crypto/rand"
"encoding/hex"
"errors"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
......@@ -33,6 +31,8 @@ import (
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/rpc"
"golang.org/x/net/context"
)
var (
......@@ -202,7 +202,7 @@ func (s *PublicFilterAPI) NewPendingTransactionFilter() (string, error) {
}
// newLogFilter creates a new log filter.
func (s *PublicFilterAPI) newLogFilter(earliest, latest int64, addresses []common.Address, topics [][]common.Hash) (int, error) {
func (s *PublicFilterAPI) newLogFilter(earliest, latest int64, addresses []common.Address, topics [][]common.Hash, callback func(log *vm.Log, removed bool)) (int, error) {
s.logMu.Lock()
defer s.logMu.Unlock()
......@@ -219,17 +219,70 @@ func (s *PublicFilterAPI) newLogFilter(earliest, latest int64, addresses []commo
filter.SetAddresses(addresses)
filter.SetTopics(topics)
filter.LogCallback = func(log *vm.Log, removed bool) {
s.logMu.Lock()
defer s.logMu.Unlock()
if queue := s.logQueue[id]; queue != nil {
queue.add(vmlog{log, removed})
if callback != nil {
callback(log, removed)
} else {
s.logMu.Lock()
defer s.logMu.Unlock()
if queue := s.logQueue[id]; queue != nil {
queue.add(vmlog{log, removed})
}
}
}
return id, nil
}
func (s *PublicFilterAPI) Logs(ctx context.Context, args NewFilterArgs) (rpc.Subscription, error) {
notifier, supported := ctx.Value(rpc.NotifierContextKey).(rpc.Notifier)
if !supported {
return nil, rpc.ErrNotificationsUnsupported
}
var (
externalId string
subscription rpc.Subscription
err error
)
if externalId, err = newFilterId(); err != nil {
return nil, err
}
// uninstall filter when subscription is unsubscribed/cancelled
if subscription, err = notifier.NewSubscription(func(string) {
s.UninstallFilter(externalId)
}); err != nil {
return nil, err
}
notifySubscriber := func(log *vm.Log, removed bool) {
rpcLog := toRPCLogs(vm.Logs{log}, removed)
if err := subscription.Notify(rpcLog); err != nil {
subscription.Cancel()
}
}
// from and to block number are not used since subscriptions don't allow you to travel to "time"
var id int
if len(args.Addresses) > 0 {
id, err = s.newLogFilter(-1, -1, args.Addresses, args.Topics, notifySubscriber)
} else {
id, err = s.newLogFilter(-1, -1, nil, args.Topics, notifySubscriber)
}
if err != nil {
subscription.Cancel()
return nil, err
}
s.filterMapMu.Lock()
s.filterMapping[externalId] = id
s.filterMapMu.Unlock()
return subscription, err
}
// NewFilterArgs represents a request to create a new filter.
type NewFilterArgs struct {
FromBlock rpc.BlockNumber
......@@ -364,9 +417,9 @@ func (s *PublicFilterAPI) NewFilter(args NewFilterArgs) (string, error) {
var id int
if len(args.Addresses) > 0 {
id, err = s.newLogFilter(args.FromBlock.Int64(), args.ToBlock.Int64(), args.Addresses, args.Topics)
id, err = s.newLogFilter(args.FromBlock.Int64(), args.ToBlock.Int64(), args.Addresses, args.Topics, nil)
} else {
id, err = s.newLogFilter(args.FromBlock.Int64(), args.ToBlock.Int64(), nil, args.Topics)
id, err = s.newLogFilter(args.FromBlock.Int64(), args.ToBlock.Int64(), nil, args.Topics, nil)
}
if err != nil {
return "", err
......
......@@ -303,7 +303,7 @@ func (n *Node) startIPC(apis []rpc.API) error {
glog.V(logger.Error).Infof("IPC accept failed: %v", err)
continue
}
go handler.ServeCodec(rpc.NewJSONCodec(conn))
go handler.ServeCodec(rpc.NewJSONCodec(conn), rpc.OptionMethodInvocation | rpc.OptionSubscriptions)
}
}()
// All listeners booted successfully
......
......@@ -68,35 +68,19 @@ The package also supports the publish subscribe pattern through the use of subsc
A method that is considered eligible for notifications must satisfy the following criteria:
- object must be exported
- method must be exported
- first method argument type must be context.Context
- method argument(s) must be exported or builtin types
- method must return the tuple Subscription, error
An example method:
func (s *BlockChainService) Head() (Subscription, error) {
sub := s.bc.eventMux.Subscribe(ChainHeadEvent{})
return v2.NewSubscription(sub), nil
}
This method will push all raised ChainHeadEvents to subscribed clients. If the client is only
interested in every N'th block it is possible to add a criteria.
func (s *BlockChainService) HeadFiltered(nth uint64) (Subscription, error) {
sub := s.bc.eventMux.Subscribe(ChainHeadEvent{})
criteria := func(event interface{}) bool {
chainHeadEvent := event.(ChainHeadEvent)
if chainHeadEvent.Block.NumberU64() % nth == 0 {
return true
}
return false
}
return v2.NewSubscriptionFiltered(sub, criteria), nil
func (s *BlockChainService) NewBlocks(ctx context.Context) (Subscription, error) {
...
}
Subscriptions are deleted when:
- the user sends an unsubscribe request
- the connection which was used to create the subscription is closed
- the connection which was used to create the subscription is closed. This can be initiated
by the client and server. The server will close the connection on an write error or when
the queue of buffered notifications gets too big.
*/
package rpc
......@@ -126,7 +126,7 @@ func newJSONHTTPHandler(srv *Server) http.HandlerFunc {
// a single request.
codec := NewJSONCodec(&httpReadWriteNopCloser{r.Body, w})
defer codec.Close()
srv.ServeSingleRequest(codec)
srv.ServeSingleRequest(codec, OptionMethodInvocation)
}
}
......
......@@ -39,7 +39,7 @@ func (c *inProcClient) Close() {
// RPC server.
func NewInProcRPCClient(handler *Server) Client {
p1, p2 := net.Pipe()
go handler.ServeCodec(NewJSONCodec(p1))
go handler.ServeCodec(NewJSONCodec(p1), OptionMethodInvocation|OptionSubscriptions)
return &inProcClient{handler, p2, json.NewEncoder(p2), json.NewDecoder(p2)}
}
......
......@@ -22,7 +22,7 @@ import (
"io"
"reflect"
"strings"
"sync/atomic"
"sync"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
......@@ -81,19 +81,20 @@ type jsonNotification struct {
// jsonCodec reads and writes JSON-RPC messages to the underlying connection. It also has support for parsing arguments
// and serializing (result) objects.
type jsonCodec struct {
closed chan interface{}
isClosed int32
d *json.Decoder
e *json.Encoder
req JSONRequest
rw io.ReadWriteCloser
closed chan interface{}
closer sync.Once
d *json.Decoder
muEncoder sync.Mutex
e *json.Encoder
req JSONRequest
rw io.ReadWriteCloser
}
// NewJSONCodec creates a new RPC server codec with support for JSON-RPC 2.0
func NewJSONCodec(rwc io.ReadWriteCloser) ServerCodec {
d := json.NewDecoder(rwc)
d.UseNumber()
return &jsonCodec{closed: make(chan interface{}), d: d, e: json.NewEncoder(rwc), rw: rwc, isClosed: 0}
return &jsonCodec{closed: make(chan interface{}), d: d, e: json.NewEncoder(rwc), rw: rwc}
}
// isBatch returns true when the first non-whitespace characters is '['
......@@ -326,15 +327,18 @@ func (c *jsonCodec) CreateNotification(subid string, event interface{}) interfac
// Write message to client
func (c *jsonCodec) Write(res interface{}) error {
c.muEncoder.Lock()
defer c.muEncoder.Unlock()
return c.e.Encode(res)
}
// Close the underlying connection
func (c *jsonCodec) Close() {
if atomic.CompareAndSwapInt32(&c.isClosed, 0, 1) {
c.closer.Do(func() {
close(c.closed)
c.rw.Close()
}
})
}
// Closed returns a channel which will be closed when Close is called
......
This diff is collapsed.
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"encoding/json"
"net"
"testing"
"time"
"golang.org/x/net/context"
)
type NotificationTestService struct{}
var (
unsubCallbackCalled = false
)
func (s *NotificationTestService) Unsubscribe(subid string) {
unsubCallbackCalled = true
}
func (s *NotificationTestService) SomeSubscription(ctx context.Context, n, val int) (Subscription, error) {
notifier, supported := ctx.Value(NotifierContextKey).(Notifier)
if !supported {
return nil, ErrNotificationsUnsupported
}
// by explicitly creating an subscription we make sure that the subscription id is send back to the client
// before the first subscription.Notify is called. Otherwise the events might be send before the response
// for the eth_subscribe method.
subscription, err := notifier.NewSubscription(s.Unsubscribe)
if err != nil {
return nil, err
}
go func() {
for i := 0; i < n; i++ {
if err := subscription.Notify(val + i); err != nil {
return
}
}
}()
return subscription, nil
}
func TestNotifications(t *testing.T) {
server := NewServer()
service := &NotificationTestService{}
if err := server.RegisterName("eth", service); err != nil {
t.Fatalf("unable to register test service %v", err)
}
clientConn, serverConn := net.Pipe()
go server.ServeCodec(NewJSONCodec(serverConn), OptionMethodInvocation|OptionSubscriptions)
out := json.NewEncoder(clientConn)
in := json.NewDecoder(clientConn)
n := 5
val := 12345
request := map[string]interface{}{
"id": 1,
"method": "eth_subscribe",
"version": "2.0",
"params": []interface{}{"someSubscription", n, val},
}
// create subscription
if err := out.Encode(request); err != nil {
t.Fatal(err)
}
var subid string
response := JSONSuccessResponse{Result: subid}
if err := in.Decode(&response); err != nil {
t.Fatal(err)
}
var ok bool
if subid, ok = response.Result.(string); !ok {
t.Fatalf("expected subscription id, got %T", response.Result)
}
for i := 0; i < n; i++ {
var notification jsonNotification
if err := in.Decode(&notification); err != nil {
t.Fatalf("%v", err)
}
if int(notification.Params.Result.(float64)) != val+i {
t.Fatalf("expected %d, got %d", val+i, notification.Params.Result)
}
}
clientConn.Close() // causes notification unsubscribe callback to be called
time.Sleep(1 * time.Second)
if !unsubCallbackCalled {
t.Error("unsubscribe callback not called after closing connection")
}
}
This diff is collapsed.
......@@ -65,8 +65,12 @@ func (s *Service) InvalidRets3() (string, string, error) {
return "", "", nil
}
func (s *Service) Subscription() (Subscription, error) {
return NewSubscription(nil), nil
func (s *Service) Subscription(ctx context.Context) (Subscription, error) {
return nil, nil
}
func (s *Service) SubsriptionWithArgs(ctx context.Context, a, b int) (Subscription, error) {
return nil, nil
}
func TestServerRegisterName(t *testing.T) {
......@@ -90,8 +94,8 @@ func TestServerRegisterName(t *testing.T) {
t.Errorf("Expected 4 callbacks for service 'calc', got %d", len(svc.callbacks))
}
if len(svc.subscriptions) != 1 {
t.Errorf("Expected 1 subscription for service 'calc', got %d", len(svc.subscriptions))
if len(svc.subscriptions) != 2 {
t.Errorf("Expected 2 subscriptions for service 'calc', got %d", len(svc.subscriptions))
}
}
......@@ -229,7 +233,7 @@ func TestServerMethodExecution(t *testing.T) {
input, _ := json.Marshal(&req)
codec := &ServerTestCodec{input: input, closer: make(chan interface{})}
go server.ServeCodec(codec)
go server.ServeCodec(codec, OptionMethodInvocation)
<-codec.closer
......@@ -259,7 +263,7 @@ func TestServerMethodWithCtx(t *testing.T) {
input, _ := json.Marshal(&req)
codec := &ServerTestCodec{input: input, closer: make(chan interface{})}
go server.ServeCodec(codec)
go server.ServeCodec(codec, OptionMethodInvocation)
<-codec.closer
......
......@@ -24,7 +24,6 @@ import (
"strings"
"sync"
"github.com/ethereum/go-ethereum/event"
"gopkg.in/fatih/set.v0"
)
......@@ -66,10 +65,10 @@ type serverRequest struct {
err RPCError
}
type serviceRegistry map[string]*service // collection of services
type callbacks map[string]*callback // collection of RPC callbacks
type subscriptions map[string]*callback // collection of subscription callbacks
type subscriptionRegistry map[string]Subscription // collection of subscriptions
type serviceRegistry map[string]*service // collection of services
type callbacks map[string]*callback // collection of RPC callbacks
type subscriptions map[string]*callback // collection of subscription callbacks
type subscriptionRegistry map[string]*callback // collection of subscription callbacks
// Server represents a RPC server
type Server struct {
......@@ -123,51 +122,6 @@ type ServerCodec interface {
Closed() <-chan interface{}
}
// SubscriptionMatcher returns true if the given value matches the criteria specified by the user
type SubscriptionMatcher func(interface{}) bool
// SubscriptionOutputFormat accepts event data and has the ability to format the data before it is send to the client
type SubscriptionOutputFormat func(interface{}) interface{}
// defaultSubscriptionOutputFormatter returns data and is used as default output format for notifications
func defaultSubscriptionOutputFormatter(data interface{}) interface{} {
return data
}
// Subscription is used by the server to send notifications to the client
type Subscription struct {
sub event.Subscription
match SubscriptionMatcher
format SubscriptionOutputFormat
}
// NewSubscription create a new RPC subscription
func NewSubscription(sub event.Subscription) Subscription {
return Subscription{sub, nil, defaultSubscriptionOutputFormatter}
}
// NewSubscriptionWithOutputFormat create a new RPC subscription which a custom notification output format
func NewSubscriptionWithOutputFormat(sub event.Subscription, formatter SubscriptionOutputFormat) Subscription {
return Subscription{sub, nil, formatter}
}
// NewSubscriptionFiltered will create a new subscription. For each raised event the given matcher is
// called. If it returns true the event is send as notification to the client, otherwise it is ignored.
func NewSubscriptionFiltered(sub event.Subscription, match SubscriptionMatcher) Subscription {
return Subscription{sub, match, defaultSubscriptionOutputFormatter}
}
// Chan returns the channel where new events will be published. It's up the user to call the matcher to
// determine if the events are interesting for the client.
func (s *Subscription) Chan() <-chan *event.Event {
return s.sub.Chan()
}
// Unsubscribe will end the subscription and closes the event channel
func (s *Subscription) Unsubscribe() {
s.sub.Unsubscribe()
}
// HexNumber serializes a number to hex format using the "%#x" format
type HexNumber big.Int
......
......@@ -45,6 +45,16 @@ func isExportedOrBuiltinType(t reflect.Type) bool {
return isExported(t.Name()) || t.PkgPath() == ""
}
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
// isContextType returns an indication if the given t is of context.Context or *context.Context type
func isContextType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t == contextType
}
var errorType = reflect.TypeOf((*error)(nil)).Elem()
// Implements this type the error interface
......@@ -57,6 +67,7 @@ func isErrorType(t reflect.Type) bool {
var subscriptionType = reflect.TypeOf((*Subscription)(nil)).Elem()
// isSubscriptionType returns an indication if the given t is of Subscription or *Subscription type
func isSubscriptionType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
......@@ -64,12 +75,17 @@ func isSubscriptionType(t reflect.Type) bool {
return t == subscriptionType
}
// isPubSub tests whether the given method return the pair (v2.Subscription, error)
// isPubSub tests whether the given method has as as first argument a context.Context
// and returns the pair (Subscription, error)
func isPubSub(methodType reflect.Type) bool {
if methodType.NumOut() != 2 {
// numIn(0) is the receiver type
if methodType.NumIn() < 2 || methodType.NumOut() != 2 {
return false
}
return isSubscriptionType(methodType.Out(0)) && isErrorType(methodType.Out(1))
return isContextType(methodType.In(1)) &&
isSubscriptionType(methodType.Out(0)) &&
isErrorType(methodType.Out(1))
}
// formatName will convert to first character to lower case
......@@ -110,8 +126,6 @@ func isBlockNumber(t reflect.Type) bool {
return t == blockNumberType
}
var contextType = reflect.TypeOf(new(context.Context)).Elem()
// suitableCallbacks iterates over the methods of the given type. It will determine if a method satisfies the criteria
// for a RPC callback or a subscription callback and adds it to the collection of callbacks or subscriptions. See server
// documentation for a summary of these criteria.
......@@ -205,7 +219,7 @@ METHODS:
return callbacks, subscriptions
}
func newSubscriptionId() (string, error) {
func newSubscriptionID() (string, error) {
var subid [16]byte
n, _ := rand.Read(subid[:])
if n != 16 {
......
......@@ -93,7 +93,8 @@ func NewWSServer(cors string, handler *Server) *http.Server {
Handler: websocket.Server{
Handshake: wsHandshakeValidator(strings.Split(cors, ",")),
Handler: func(conn *websocket.Conn) {
handler.ServeCodec(NewJSONCodec(&wsReaderWriterCloser{conn}))
handler.ServeCodec(NewJSONCodec(&wsReaderWriterCloser{conn}),
OptionMethodInvocation|OptionSubscriptions)
},
},
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment