前言
距離上一篇拖了快一個月XD,之前因為工作較忙而且準備英文考試比較沒有時間來寫 blog,最近趁著空檔來把上次沒完成的把它寫完。上篇說到利用 shared library
可以將串接 GRPC service 的 code 統一包裝起來,這樣子維護以及可用性會比較好,尤其當你是多人開發遇到這個情況時,感受會更為顯著,且整體 team 間彼此的 code 重複性不會太多。那麼同仁們在使用時遇到了一些問題,後來筆者發現其實有寫 Unit testing
的話便可以盡早發現這些問題,因此這篇筆者將會分享同仁們使用時遇到了哪些問題,筆者該怎麼去測 GRPC function。
最後,同場加映一下筆者為了加速公司 CI/CD 流程,為公司引入了 Golang proxy
的機制,這邊也分享一下。
Unit testing
延續上一篇的 GRPC service 的 proto,我們今天要如何來對每個 wrapper 對應到的 GRPC function 去做測試呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
syntax = "proto3";
service systemMessage {
rpc getInfoMsg (getRequest) returns (getResponse) {}
rpc getWarningMsg (getRequest) returns (getResponse) {}
rpc postSysInfo (addRequest) returns (addResponse) {}
rpc postSysWarning (addRequest) returns (addResponse) {}
...
}
舉例來說,根據上篇文章假設筆者用 function: Info 去包裝 GRPC function: postSysInfo,用途是要送有關 information 的 message。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Info is to send messages to systemmessage, assign title and message in parameter field
func (s *SystemMsg) Info(title string, message string) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
if s.Conn == nil {
return
}
if s.Client != nil {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := s.Client.PostSysInfo(ctx, &spec.PostSysInfoRequest{
Module: s.Module,
Title: title,
Message: message,
})
//if send message failed, print the error msg on console
if err != nil {
log.Printf("Send system message failed, err: %s\n", err)
}
}
}
因此,筆者這邊會針對包裝過起來的 function 去做 unit testing
。
Mock GRPC server
首先,我們先將我們要測的 GRPC server 做一個假的起來。所以編寫 mock_sysmsg.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package sysmsg
//This is a mock GRPC server for systemwarning.
//Each rpc function just return 0 for status code and write request string into buffer.
import (
"bytes"
"context"
"fmt"
"net"
spec "sharedlib/pb/systemwarning"
"sync"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
//MockServer for testing
var MockServer *MockmessageServer
//define grpc server and run once function
var grpcServer *grpc.Server
var grpcSync sync.Once
//MockmessageServer define
type MockmessageServer struct {
bufString *bytes.Buffer
}
//PostWarning define
func (ms *MockmessageServer) PostSysInfo(ctx context.Context, req *spec.AddRequest) (*spec.GeneralResponse, error) {
response := &spec.GeneralResponse{
StatusCode: 0,
Message: "success",
}
ms.bufString.WriteString(req.String())
return response, nil
}
func initialServer() {
grpcSync.Do(func() {
grpcServer = grpc.NewServer()
fmt.Println("Start insecured gRPC server")
})
}
//StopGRPCServer gracefully
func StopGRPCServer() {
grpcServer.GracefulStop()
}
//StartGRPCServer start grpc server on target port
func StartGRPCServer(port string) {
initialServer()
lis, err := net.Listen("tcp", ":"+port)
if err != nil {
fmt.Printf("failed to listen: %v\n", err)
return
}
var buf bytes.Buffer
grpcServer = grpc.NewServer()
MockServer = &MockmessageServer{
bufString: &buf,
}
spec.RegisterSystemWarningServer(grpcServer, MockServer)
reflection.Register(grpcServer)
if err := grpcServer.Serve(lis); err != nil {
fmt.Printf("failed to serve: %v\n", err)
return
}
}
這邊筆者只列出 PostSysInfo
來實作,其餘讀者可以自行補充。
有寫過 GRPC 的朋友應該都知道,我們 backup-end 會需要去實作 proto 裡面所定義出來的 GRPC function。那其實對於我們 wrapper 的 function 來說,back-end 裡面的實際邏輯對我們來說是甚麼我們其實不需要知道,我們就可以來做 testing 了! 這個概念是甚麼呢? 假設這個 GRPC server 是另外一個同仁在實作,但是當他還沒完成的時候,我們就只能乾等直到他寫好之後我們才能跟他串起來嗎? 當然不是,我們可以利用我們自己 mock 起來的 server 就能測到一些 error handling 的機制。
Single testing of a GRPC function
以剛剛上面的 mock server 為例子,筆者將 PostSysInfo
簡單地實作成,把收到的 request 塞進一個 buffer 裡面,再回傳給 client。所以 testing code 來測包裝 PostSysInfo
的 Info function 就會像是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package sysmsg
import (
"fmt"
"strings"
"testing"
"time"
)
var sysMsg *SystemMsg
func initMockServer() {
go StartGRPCServer("9876")
time.Sleep(3 * time.Second)
sysMsg = NewSystemMessageSender("test", "localhost:9876")
}
func TestSendSystemInfo(t *testing.T) {
initMockServer()
message := "This message is an info message"
sysMsg.Info("testing", message)
res := MockServer.bufString.String()
if !strings.Contains(res, message) {
t.Fail()
}
defer teardown()
}
func teardown() {
sysMsg.Close()
StopGRPCServer()
}
從 buffer 讀取字串,看是否 message 符合預期的字串。
When GRPC server is dead
前面有提到同仁遇到的問題,那就是當 GRPC server 不通或是他掛掉的時候,筆者這邊 wrapper function 沒有處理到這塊。那麼如果筆者有寫下面這個 testing 的話就會抓到這個情況:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func TestSendFailed(t *testing.T) {
initMockServer()
//send a message
message := "This message is a message"
sysMsg.Info("testing", message)
res := MockServer.bufString.String()
if !strings.Contains(res, message) {
t.Fail()
}
//stop server
StopGRPCServer()
//try to send a message again
message = "This message should not exists"
sysMsg.Info("testing", message)
res = MockServer.bufString.String()
if strings.Contains(res, message) {
t.Fail()
}
defer teardown()
}
故意在中間將 mock server 給關掉,試著模擬連不到的情況。以之前 wrapper 的實作方式,連不到的時候將會卡住在要把 message 送出去的部分,因為 GRPC 有自己嘗試重連的機制,那筆者這邊的 context 又沒有設 timeout 的話就會永遠卡住。但是設 timeout 的話,比如說 5 秒,那當連不到的時候又會每次都卡 5 秒才會繼續往下 go。可能會導致你的 module 有使用到這個 function 的 code 都會被拖著 5 秒。
1
2
3
4
5
6
7
8
9
10
11
12
13
if s.Client != nil {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := s.Client.PostSysInfo(ctx, &spec.PostSysInfoRequest{
Module: s.Module,
Title: title,
Message: message,
})
//if send message failed, print the error msg on console
if err != nil {
log.Printf("Send system message failed, err: %s\n", err)
}
}
因此,筆者這邊詢問了一下 google 大叔的建議,找了一下有沒有關於可以 check GRPC connection 的方式。答案是有的,可以使用 GetState,但是這些 function 在官方文件上的註解有寫說:
1
2
3
Experimental
Notice: This API is EXPERIMENTAL and may be changed or removed in a later release
但是筆者使用上其實是沒有問題的,GetState 可以用來 check 目前 connection 的狀態,就可以先檢查連接狀態來決定是否要繼續送 message。
1
2
3
4
5
state := s.Conn.GetState().String()
if state != "READY" && state != "IDLE" {
log.Printf("Can not connect to sysmsg module, sysmsg module state is %s\n", state)
return
}
若不是 READY
或是 IDLE
,則直接寫 log 並且直接 return。這樣一來,當 GRPC server 連不到,就不會卡住並且能夠從 log 直接判斷連接中斷的部分。
小結
Unit testing
很重要,可以幫助你抓出一些非預期性的情況,模擬這些情況並且處理好 error handling。除此之外,也不會與別人有工作上的 dependency,整合可以更為快速。
Golang proxy
最後,分享一下,筆者幫助公司引進了在 build code 時,能夠節省大半時間的 golang proxy
。
1
2
3
export athens_storage=~/athens-storage
mkdir -p $athens_storage
docker run -d -v $athens_storage:/var/lib/athens \ -e athens_disk_storage_root=/var/lib/athens \ -e athens_storage_type=disk \ --name athens-proxy \ --restart always \ -p 3000:3000 \ gomods/athens:latest
各位夥伴也可以去 google 一下有關 golang build proxy 的關鍵字。簡單來說,在 build machine 去帶起來一個 container,它會去儲存你要 build 的 project 所需要的套件,那麼下一次要再重新 build code 的時候,就不會去網路上面拉,會在此 proxy 拉。除非你更新了所使用的套件版本或是有新套件的使用,他才會去網路上面尋找。
筆者公司是使用 dockerfile 來 build golang 的 project。如以下範例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM golang:1.12-alpine AS go-builder
LABEL stage=build
ENV GO111MODULE on
ARG GOLANG_PROXY_IP
ENV GOPROXY=http://${GOLANG_PROXY_IP}:3000
WORKDIR /go/src/kubeshell
RUN apk add git && \
cd /go/src/kubeshell && \
CGO_ENABLE=0 GOOS=linux GOARCH=amd64 go build -o app main.go
...
將環境變數 GOPROXY
指向到你的 build machine,也就是你上面 container 帶起來的所在機器 IP,不一定要 3000 port,在上方的 docker run command 中去改變即可。印象中,引進此機制後,從原本快一個小時的 build job,縮短到 20 多分鐘,當你 project 越多時,效果一定越為顯著喔!
Contact me
有任何疑問或是建議歡迎聯絡 Jimmy!
Gmail: jimmyw86878@gmail.com
-
Previous
Golang - GRPC function wrapper in shared library -
Next
Ansible - Ansible cluster sandbox for testing, scaling and learning your playbooks