Skip to content

Commit

Permalink
Add model ready waiting logic to resolve issue intel-retail#331 (inte…
Browse files Browse the repository at this point in the history
…l-retail#333)

* feat: use ovms api to better handle model ready state
* fix: resolve the issue for running multiple more than 5 pipelines
 - Add gRPC calls in profile-launcher to test models readiness from ovms server
 - Refactor how the unit-test makefile runs as now it requires google gRPC packages

 FIXES: intel-retail#331

* refactor: change the go module package definition from vision-self-checkout to automated-self-checkout
* feat: upgrade goolge gRPC and protobuf to the latest version

Signed-off-by: Jim Wang <[email protected]>

---------

Signed-off-by: Jim Wang <[email protected]>
  • Loading branch information
jim-wang-intel authored Nov 6, 2023
1 parent c49d56b commit f248acd
Show file tree
Hide file tree
Showing 15 changed files with 397 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ model_server/
ssd_mobilenet_v1_coco/
__pycache__
smoke_tests_output.log
grpc_predict_v2.proto
13 changes: 11 additions & 2 deletions configs/opencv-ovms/cmd_client/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@
#

FROM golang:1.20.9-alpine3.18 AS builder
RUN apk update && apk add make
RUN apk update && \
apk add make autoconf libtool protobuf-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN go install google.golang.org/protobuf/cmd/[email protected]
RUN go install google.golang.org/grpc/cmd/[email protected]
# Compile API
RUN wget https://raw.githubusercontent.com/openvinotoolkit/model_server/main/src/kfserving_api/grpc_predict_v2.proto
RUN echo 'option go_package = "./grpc-client";' >> grpc_predict_v2.proto
RUN protoc --go_out="./" --go-grpc_out="./" ./grpc_predict_v2.proto

COPY . .
RUN make build-binary
RUN go mod tidy && make build-binary

FROM scratch as bin
COPY --from=builder /app/profile-launcher /
21 changes: 21 additions & 0 deletions configs/opencv-ovms/cmd_client/Dockerfile.unit-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#
# Copyright (C) 2023 Intel Corporation.
#
# SPDX-License-Identifier: Apache-2.0
#

FROM golang:1.20.9-alpine3.18 AS builder
RUN apk update && \
apk add make autoconf libtool protobuf-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /test

# install docker, as we need docker for some unit tests
RUN apk update && apk add --update docker openrc && \
rm -rf /var/lib/apt/lists/*

COPY . .

RUN chmod +x /test/run_unit_test.sh

ENTRYPOINT ["/test/run_unit_test.sh"]
3 changes: 2 additions & 1 deletion configs/opencv-ovms/cmd_client/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ build:
docker build --target bin --output=. .

unit-test:
go test -count=1 ./...
@docker build -f Dockerfile.unit-test -t go-unit-test:dev .
@docker run --rm -v .:/test -v /var/run/docker.sock:/var/run/docker.sock go-unit-test:dev

build-binary:
@echo "building executeable profile-launcher..."
Expand Down
9 changes: 8 additions & 1 deletion configs/opencv-ovms/cmd_client/go.mod
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
module github.com/intel-retail/vision-self-checkout/configs/opencv-ovms/cmd_client
module github.com/intel-retail/automated-self-checkout/configs/opencv-ovms/cmd_client

go 1.20

require (
github.com/stretchr/testify v1.8.4
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.31.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
)
20 changes: 20 additions & 0 deletions configs/opencv-ovms/cmd_client/go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
117 changes: 115 additions & 2 deletions configs/opencv-ovms/cmd_client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main

import (
"bufio"
"context"
"encoding/json"
"flag"
"fmt"
Expand All @@ -29,9 +30,14 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"

"github.com/intel-retail/vision-self-checkout/configs/opencv-ovms/cmd_client/portfinder"
"github.com/intel-retail/automated-self-checkout/configs/opencv-ovms/cmd_client/parser"
"github.com/intel-retail/automated-self-checkout/configs/opencv-ovms/cmd_client/portfinder"

grpc_client "github.com/intel-retail/automated-self-checkout/configs/opencv-ovms/cmd_client/grpc-client"
"google.golang.org/grpc"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -66,6 +72,10 @@ const (
streamDensityScript = "/app/stream_density.sh"
streamDensityResultDir = "/tmp/results"

ovmsConfigJsonDir = "./configs/opencv-ovms/models/2022"
ovmsTemplateConfigJson = "config_template.json"
ovmsModelReadyMaxRetries = 100

defaultOvmsServerStartWaitTime = time.Duration(10 * time.Second)
dockerVolumeFlag = "-v"
)
Expand Down Expand Up @@ -342,7 +352,110 @@ func (ovmsClientConf *OvmsClientConfig) startOvmsServer() {

log.Println("Let server settle a bit...")
time.Sleep(ovmsSrvWaitTime)
log.Println("OVMS server started")

// wait for the model ready state status:
// even though the ovms server is ready but the individual models may not be
// due to the model config file config.json changes on the fly
retryCnt := 0
for {
if retryCnt >= ovmsModelReadyMaxRetries {
log.Printf("error: reaches the max retry count %d for checking model ready of ovms server; gave up", ovmsModelReadyMaxRetries)
return
}

retryCnt++
readyErr := ovmsClientConf.waitForOvmsModelsReady()
if readyErr == nil {
// all are error free and thus models are ready
break
}

log.Printf("warning: ovms models from the model server not ready yet: %v", readyErr)
// sleep a bit and retry it again
time.Sleep(time.Second)
}

log.Println("OVMS server started and ready to serve")
}

func (ovmsClientConf *OvmsClientConfig) waitForOvmsModelsReady() error {
grpcPort := os.Getenv(GRPC_PORT_ENV)
ovmsURL := fmt.Sprintf("%s:%s", "localhost", grpcPort)
conn, err := grpc.Dial(ovmsURL, grpc.WithInsecure())
if err != nil {
return fmt.Errorf("couldn't connect to endpoint %s: %v", ovmsURL, err)
}
defer conn.Close()

// retrieve models:
ovmsModelParser := parser.NewConfigJsonModelParser(ovmsConfigJsonDir, ovmsTemplateConfigJson)
if err = ovmsModelParser.Parse(); err != nil {
return fmt.Errorf("couldn't parse OVMS config json: %v", err)
}

if len(ovmsModelParser.ModelConfigList) > 0 {
// Create client from gRPC server connection
client := grpc_client.NewGRPCInferenceServiceClient(conn)

var wg sync.WaitGroup
wg.Add(len(ovmsModelParser.ModelConfigList))
for _, model := range ovmsModelParser.ModelConfigList {
go func(model parser.ModelConfigListInfo) {
defer wg.Done()
modelName := model.Config.ModelName
// we use /1 folder under the model so the model version is always 1
modelVersion := "1"
modelReadyResponse, err := sendModelReadyRequest(client, modelName, modelVersion)
if err == nil && modelReadyResponse.Ready {
log.Printf("Model name %s with version %s is ready", modelName, modelVersion)
return
}

// this model is not in ready state yet
// we will re-test this specific model for max retry
retryCnt := 0
for {
if retryCnt >= ovmsModelReadyMaxRetries {
log.Printf("error: reaches the max retry count %d for checking model ready of ovms server; gave up", ovmsModelReadyMaxRetries)
return
}

time.Sleep(time.Second)
retryCnt++
// need new client every time since there is some cached issue if re-using the existing client
client := grpc_client.NewGRPCInferenceServiceClient(conn)
modelReadyResponse, err := sendModelReadyRequest(client, modelName, modelVersion)
if err == nil && modelReadyResponse.Ready {
log.Printf("Model name %s with version %s is ready", modelName, modelVersion)
break
}
}
}(model)
}
wg.Wait()
}
return nil
}

func sendModelReadyRequest(client grpc_client.GRPCInferenceServiceClient, modelName string, modelVersion string) (*grpc_client.ModelReadyResponse, error) {
// Create context for request with 10 second timeout
// if any request takes longer than that, we consider the system is way too slow and thus unusable
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

modelReadyRequest := grpc_client.ModelReadyRequest{
Name: modelName,
Version: modelVersion,
}

modelReadyResponse, err := client.ModelReady(ctx, &modelReadyRequest)
if err != nil {
errMsg := fmt.Errorf("Couldn't get model name %s version %s ready state: %v", modelName, modelVersion, err)
log.Println(errMsg)
return nil, errMsg
}

return modelReadyResponse, nil
}

func (ovmsClientConf *OvmsClientConfig) initDockerLauncherEnvs() {
Expand Down
69 changes: 69 additions & 0 deletions configs/opencv-ovms/cmd_client/parser/ovmsConfigJsonParser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// ----------------------------------------------------------------------------------
// Copyright 2023 Intel Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ----------------------------------------------------------------------------------

package parser

import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
)

type ModelConfigInfo struct {
ModelName string `json:"name"`
}

type ModelConfigListInfo struct {
Config ModelConfigInfo `json:"config"`
}

type ConfigJsonModelParser struct {
ModelConfigList []ModelConfigListInfo `json:"model_config_list"`

configJsonFileName string
configJsonDir string
}

func NewConfigJsonModelParser(configJsonDir, configJsonFileName string) *ConfigJsonModelParser {
return &ConfigJsonModelParser{
configJsonDir: configJsonDir,
configJsonFileName: configJsonFileName,
}
}

// Parse will parse the list of model names from the file configJsonFileName under the directory configJsonDir
func (cjmp *ConfigJsonModelParser) Parse() error {
configJsonFile := filepath.Join(cjmp.configJsonDir, cjmp.configJsonFileName)
jsonFileReader, err := os.Open(configJsonFile)
if err != nil {
return fmt.Errorf("failed to open OVMS config json file %s: %v", configJsonFile, err)
}
defer jsonFileReader.Close()

contents, err := io.ReadAll(jsonFileReader)
if err != nil {
return fmt.Errorf("failed to read OVMS config json file %s: %v", configJsonFile, err)
}

if err := json.Unmarshal(contents, &cjmp); err != nil {
return fmt.Errorf("failed to unmarshal OVMS config json: %v", err)
}

return nil
}
56 changes: 56 additions & 0 deletions configs/opencv-ovms/cmd_client/parser/ovmsConfigJsonParser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// ----------------------------------------------------------------------------------
// Copyright 2023 Intel Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ----------------------------------------------------------------------------------

package parser

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestParse(t *testing.T) {
tests := []struct {
name string
jsonFileDir string
jsonFileName string
expectError bool
}{
{"current template json file", ".", "test-config_template.json", false},
{"invalid json content", ".", "test-invalid.json", true},
{"non-existing json file", ".", "non-existing.json", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configParser := NewConfigJsonModelParser(tt.jsonFileDir, tt.jsonFileName)

err := configParser.Parse()
if !tt.expectError {
require.NoError(t, err)
require.NotEmpty(t, configParser)
require.Contains(t, configParser.ModelConfigList, ModelConfigListInfo{
Config: ModelConfigInfo{
ModelName: "efficientnet-b0",
},
})
} else {
require.Error(t, err)
}
})
}
}
Loading

0 comments on commit f248acd

Please sign in to comment.