RPC Replay - Package cloud.google.com/go/rpcreplay (v0.98.0)

Package rpcreplay supports the capture and replay of gRPC calls. Its main goal is to improve testing. Once you capture the calls of a test that runs against a real service, you have an "automatic mock" that can be replayed against the same test, yielding a unit test that is fast and flake-free.

This package is EXPERIMENTAL and subject to change without notice.

Recording

To record a sequence of gRPC calls to a file, create a Recorder and pass its DialOptions to grpc.Dial:

 rec 
 , 
  
 err 
  
 := 
  
 rpcreplay 
 . 
 NewRecorder 
 ( 
 "service.replay" 
 , 
  
 nil 
 ) 
 if 
  
 err 
  
 != 
  
 nil 
  
 { 
  
 ... 
  
 } 
 defer 
  
 func 
 () 
  
 { 
  
 if 
  
 err 
  
 := 
  
 rec 
 . 
 Close 
 (); 
  
 err 
  
 != 
  
 nil 
  
 { 
  
 ... 
  
 } 
 }() 
 conn 
 , 
  
 err 
  
 := 
  
 grpc 
 . 
 Dial 
 ( 
 serverAddress 
 , 
  
 rec 
 . 
 DialOptions 
 () 
 ... 
 ) 

It is essential to close the Recorder when the interaction is finished.

There is also a NewRecorderWriter function for capturing to an arbitrary io.Writer.

Replaying

Replaying a captured file looks almost identical: create a Replayer and use its DialOptions. (Since we're reading the file and not writing it, we don't have to be as careful about the error returned from Close).

 rep 
 , 
  
 err 
  
 := 
  
 rpcreplay 
 . 
 NewReplayer 
 ( 
 "service.replay" 
 ) 
 if 
  
 err 
  
 != 
  
 nil 
  
 { 
  
 ... 
  
 } 
 defer 
  
 rep 
 . 
 Close 
 () 
 conn 
 , 
  
 err 
  
 := 
  
 grpc 
 . 
 Dial 
 ( 
 serverAddress 
 , 
  
 rep 
 . 
 DialOptions 
 () 
 ... 
 ) 

Since a real connection isn't necessary for replay, you can get a fake one from the replayer instead of calling grpc.Dial:

 rep 
 , 
  
 err 
  
 := 
  
 rpcreplay 
 . 
 NewReplayer 
 ( 
 "service.replay" 
 ) 
 if 
  
 err 
  
 != 
  
 nil 
  
 { 
  
 ... 
  
 } 
 defer 
  
 rep 
 . 
 Close 
 () 
 conn 
 , 
  
 err 
  
 := 
  
 rep 
 . 
 Connection 
 () 

Initial State

A test might use random or time-sensitive values, for instance to create unique resources for isolation from other tests. The test therefore has initial values, such as the current time, or a random seed, that differ from run to run. You must record this initial state and re-establish it on replay.

To record the initial state, serialize it into a []byte and pass it as the second argument to NewRecorder:

 timeNow 
  
 := 
  
 time 
 . 
 Now 
 () 
 b 
 , 
  
 err 
  
 := 
  
 timeNow 
 . 
 MarshalBinary 
 () 
 if 
  
 err 
  
 != 
  
 nil 
  
 { 
  
 ... 
  
 } 
 rec 
 , 
  
 err 
  
 := 
  
 rpcreplay 
 . 
 NewRecorder 
 ( 
 "service.replay" 
 , 
  
 b 
 ) 

On replay, get the bytes from Replayer.Initial:

 rep 
 , 
  
 err 
  
 := 
  
 rpcreplay 
 . 
 NewReplayer 
 ( 
 "service.replay" 
 ) 
 if 
  
 err 
  
 != 
  
 nil 
  
 { 
  
 ... 
  
 } 
 defer 
  
 rep 
 . 
 Close 
 () 
 err 
  
 = 
  
 timeNow 
 . 
 UnmarshalBinary 
 ( 
 rep 
 . 
 Initial 
 ()) 
 if 
  
 err 
  
 != 
  
 nil 
  
 { 
  
 ... 
  
 } 

Callbacks

Recorders and replayers have support for running callbacks before messages are written to or read from the replay file. A Recorder has a BeforeFunc that can modify a request or response before it is written to the replay file. The actual RPCs sent to the service during recording remain unaltered; only what is saved in the replay file can be changed. A Replayer has a BeforeFunc that can modify a request before it is sent for matching.

Example uses for these callbacks include customized logging, or scrubbing data before RPCs are written to the replay file. If requests are modified by the callbacks during recording, it is important to perform the same modifications to the requests when replaying, or RPC matching on replay will fail.

A common way to analyze and modify the various messages is to use a type switch.

 // Assume these types implement proto.Message. 
 type 
  
 Greeting 
  
 struct 
  
 { 
  
 line 
  
 string 
 } 
 type 
  
 Farewell 
  
 struct 
  
 { 
  
 line 
  
 string 
 } 
 func 
  
 sayings 
 ( 
 method 
  
 string 
 , 
  
 msg 
  
 proto 
 . 
 Message 
 ) 
  
 error 
  
 { 
  
 switch 
  
 m 
  
 := 
  
 msg 
 .( 
 type 
 ) 
  
 { 
  
 case 
  
 Greeting 
 : 
  
 msg 
 . 
 line 
  
 = 
  
 "Hi!" 
  
 return 
  
 nil 
  
 case 
  
 Farewell 
 : 
  
 msg 
 . 
 line 
  
 = 
  
 "Bye bye!" 
  
 return 
  
 nil 
  
 default 
 : 
  
 return 
  
 fmt 
 . 
 Errorf 
 ( 
 "unknown message type" 
 ) 
  
 } 
 } 

Nondeterminism

A nondeterministic program may invoke RPCs in a different order each time it is run. The order in which RPCs are called during recording may differ from the order during replay.

The replayer matches incoming to recorded requests by method name and request contents, so nondeterminism is only a concern for identical requests that result in different responses. A nondeterministic program whose behavior differs depending on the order of such RPCs probably has a race condition: since both the recorded sequence of RPCs and the sequence during replay are valid orderings, the program should behave the same under both.

The same is not true of streaming RPCs. The replayer matches streams only by method name, since it has no other information at the time the stream is opened. Two streams with the same method name that are started concurrently may replay in the wrong order.

Other Replayer Differences

Besides the differences in replay mentioned above, other differences may cause issues for some programs. We list them here.

The Replayer delivers a response to an RPC immediately, without waiting for other incoming RPCs. This can violate causality. For example, in a Pub/Sub program where one goroutine publishes and another subscribes, during replay the Subscribe call may finish before the Publish call begins.

For streaming RPCs, the Replayer delivers the result of Send and Recv calls in the order they were recorded. No attempt is made to match message contents.

At present, this package does not record or replay stream headers and trailers, or the result of the CloseSend method.

Functions

func Fprint

  func 
  
 Fprint 
 ( 
 w 
  
  io 
 
 . 
  Writer 
 
 , 
  
 filename 
  
  string 
 
 ) 
  
  error 
 
 

Fprint reads the entries from filename and writes them to w in human-readable form. It is intended for debugging.

func FprintReader

  func 
  
 FprintReader 
 ( 
 w 
  
  io 
 
 . 
  Writer 
 
 , 
  
 r 
  
  io 
 
 . 
  Reader 
 
 ) 
  
  error 
 
 

FprintReader reads the entries from r and writes them to w in human-readable form. It is intended for debugging.

Recorder

  type 
  
 Recorder 
  
 struct 
  
 { 
  
 // BeforeFunc defines a function that can inspect and modify requests and responses 
  
 // written to the replay file. It does not modify messages sent to the service. 
  
 // It is run once before a request is written to the replay file, and once before a response 
  
 // is written to the replay file. 
  
 // The function is called with the method name and the message that triggered the callback. 
  
 // If the function returns an error, the error will be returned to the client. 
  
 // This is only executed for unary RPCs; streaming RPCs are not supported. 
  
 BeforeFunc 
  
 func 
 ( 
  string 
 
 , 
  
  proto 
 
 . 
  Message 
 
 ) 
  
  error 
 
  
 // contains filtered or unexported fields 
 } 
 

A Recorder records RPCs for later playback.

func NewRecorder

  func 
  
 NewRecorder 
 ( 
 filename 
  
  string 
 
 , 
  
 initial 
  
 [] 
  byte 
 
 ) 
  
 ( 
 * 
  Recorder 
 
 , 
  
  error 
 
 ) 
 

NewRecorder creates a recorder that writes to filename. The file will also store the initial bytes for retrieval during replay.

You must call Close on the Recorder to ensure that all data is written.

func NewRecorderWriter

  func 
  
 NewRecorderWriter 
 ( 
 w 
  
  io 
 
 . 
  Writer 
 
 , 
  
 initial 
  
 [] 
  byte 
 
 ) 
  
 ( 
 * 
  Recorder 
 
 , 
  
  error 
 
 ) 
 

NewRecorderWriter creates a recorder that writes to w. The initial bytes will also be written to w for retrieval during replay.

You must call Close on the Recorder to ensure that all data is written.

func (*Recorder) Close

  func 
  
 ( 
 r 
  
 * 
  Recorder 
 
 ) 
  
 Close 
 () 
  
  error 
 
 

Close saves any unwritten information.

func (*Recorder) DialOptions

  func 
  
 ( 
 r 
  
 * 
  Recorder 
 
 ) 
  
 DialOptions 
 () 
  
 [] 
 grpc 
 . 
 DialOption 
 

DialOptions returns the options that must be passed to grpc.Dial to enable recording.

Replayer

  type 
  
 Replayer 
  
 struct 
  
 { 
  
 // BeforeFunc defines a function that can inspect and modify requests before they 
  
 // are matched for responses from the replay file. 
  
 // The function is called with the method name and the message that triggered the callback. 
  
 // If the function returns an error, the error will be returned to the client. 
  
 // This is only executed for unary RPCs; streaming RPCs are not supported. 
  
 BeforeFunc 
  
 func 
 ( 
  string 
 
 , 
  
  proto 
 
 . 
  Message 
 
 ) 
  
  error 
 
  
 // contains filtered or unexported fields 
 } 
 

A Replayer replays a set of RPCs saved by a Recorder.

func NewReplayer

  func 
  
 NewReplayer 
 ( 
 filename 
  
  string 
 
 ) 
  
 ( 
 * 
  Replayer 
 
 , 
  
  error 
 
 ) 
 

NewReplayer creates a Replayer that reads from filename.

func NewReplayerReader

  func 
  
 NewReplayerReader 
 ( 
 r 
  
  io 
 
 . 
  Reader 
 
 ) 
  
 ( 
 * 
  Replayer 
 
 , 
  
  error 
 
 ) 
 

NewReplayerReader creates a Replayer that reads from r.

func (*Replayer) Close

  func 
  
 ( 
 rep 
  
 * 
  Replayer 
 
 ) 
  
 Close 
 () 
  
  error 
 
 

Close closes the Replayer.

func (*Replayer) Connection

  func 
  
 ( 
 rep 
  
 * 
  Replayer 
 
 ) 
  
 Connection 
 () 
  
 ( 
 * 
 grpc 
 . 
 ClientConn 
 , 
  
  error 
 
 ) 
 

Connection returns a fake gRPC connection suitable for replaying.

func (*Replayer) DialOptions

  func 
  
 ( 
 rep 
  
 * 
  Replayer 
 
 ) 
  
 DialOptions 
 () 
  
 [] 
 grpc 
 . 
 DialOption 
 

DialOptions returns the options that must be passed to grpc.Dial to enable replaying.

func (*Replayer) Initial

  func 
  
 ( 
 rep 
  
 * 
  Replayer 
 
 ) 
  
 Initial 
 () 
  
 [] 
  byte 
 
 

Initial returns the initial state saved by the Recorder.

func (*Replayer) SetLogFunc

  func 
  
 ( 
 rep 
  
 * 
  Replayer 
 
 ) 
  
 SetLogFunc 
 ( 
 f 
  
 func 
 ( 
 format 
  
  string 
 
 , 
  
 v 
  
 ... 
 interface 
 {})) 
 

SetLogFunc sets a function to be used for debug logging. The function should be safe to be called from multiple goroutines.

Create a Mobile Website
View Site in Mobile | Classic
Share by: