O-RAN SC에 오픈소스 기여를 해보자 4일차 - 이슈 등록 및 REST API 추가해보기

0

opensource

목록 보기
4/6


여유롭던 회사 생활에 갑자기 숙제들이 와르르 쏟아졌다. 오픈소스 해야하는데, 머피의 법칙마냥 세상의 질서를 유지하는 뭔가가 있긴 한가보다.

이번에는 이전에 했던 것과 같이 issue를 등록하고, code를 수정해보도록 하자. 먼저 code를 보면서 문제점이 무엇이있고, 어디가 부족한 지 알아보도록 하자.

https://github.com/o-ran-sc/aiml-fw-awmf-modelmgmtservice
해당 project는 ML model에 대한 정보를 등록하여 s3와 같은 object store에 저장하는 project이다. 문제는 다음과 같았다.

  1. REST API 중에 누락된 구현들이 있다.
    1. 모든 model정보들 가져오기 없음
    2. model 정보 삭제 기능 없음
    3. model 정보 업데이트 기능 없음
  2. REST API 규칙을 잘 지키지 않았다.
  3. Test code가 너무 부족하다. 사실상 없음
  4. project 구조가 잘 구성되지 않았다.
  5. interface 반환 기법을 사용하고 있다.

이러쿵 저러쿵 여러가지 문제가 있었는데, 가장 시급한 REST API 중 누락된 기능을 추가해보도록 하자. 특히 현재 어떤 model 정보들이 있는 지 list up하는 API부터 만들도록 하자.

먼저 s3에서 bucket들을 모두 가져오고, 각 bucket에 있는 object들을 가져오는 code를 만들도록 하자.

https://docs.aws.amazon.com/ko_kr/code-library/latest/ug/go_2_s3_code_examples.html

  • core/s3_manager.go
func (s3manager *S3Manager) ListBucket() ([]ModelInfo, error) {
	input := &s3.ListBucketsInput{}
	result, err := s3manager.S3Client.ListBuckets(input)

	if err != nil {
		logging.ERROR("Can't get bucket list in s3 ", err)
		return []ModelInfo{}, err
	}

	modelInfoList := []ModelInfo{}
	for _, bucket := range result.Buckets {
		modelName := ""
		if bucket.Name != nil {
			modelName = *bucket.Name
		}

		bucketObject := s3manager.GetBucketObject(modelName+os.Getenv("INFO_FILE_POSTFIX"), modelName)
		modelInfoList = append(modelInfoList, ModelInfo{
			Name: modelName,
			Data: string(bucketObject),
		})
	}

	return modelInfoList, nil
}

다음으로 handler를 만들어서, ListBucket을 호출하고, 응답으로 전달해주기로 하자.

  • apis/mmes_apis.go
func (m *MmeApiHandler) GetModelInfoList(cont *gin.Context) {
	logging.INFO("List all model API")
	bucketList, err := m.dbmgr.ListBucket()
	if err != nil {
		statusCode := http.StatusInternalServerError
		logging.ERROR("Error occurred, send status code: ", statusCode)
		cont.JSON(statusCode, gin.H{
			"code":    http.StatusText(statusCode),
			"message": "Unexpected Error in server, you can't get model information list",
		})
		return
	}

	modelInfoListRespModel := []modelInfoResponseModel{}
	for _, bucket := range bucketList {
		modelInfoListRespModel = append(modelInfoListRespModel, modelInfoResponseModel{
			Name: bucket.Name,
			Data: bucket.Data,
		})
	}

	cont.JSON(http.StatusOK, gin.H{
		"code":    http.StatusOK,
		"message": modelInfoListRespModel,
	})
}

원작자의 coding style대로 code를 만들었는데, 원작자가 python으로 개발을 하셨던 분 같다. 응답을 보내는 code부터 시작해서 에러 처리하는 방식이 python스럽게 code가 만들어져있었다. 오픈소스를 하면 이런게 참 재밌다. 필자도 go, python 둘 다 하고있지만, go를 할 때는 go스러운 code, python할 때는 python스러운 code로 만드려고 한다. 그러나 쉽지 않다. 인간의 버릇이라는 것이 무의식 중에 code 스타일로 베이게 되는 것이다. 오픈소스를 참여하다보면 go를 go스럽게, python을 python스럽게 개발하지 못하는 분들이 많아보여서 아쉬우면서도, 재밌는 경험이 되기도 한다.

이제 동작을 test해보기 전에 매번 docker 빌드를 구동하기 귀찮으니 간단하게 스크립트를 만들어 test하도록 하자.

  • build_image.sh
sudo nerdctl run -d --name buildkitd --privileged moby/buildkit:latest
sudo nerdctl rmi modelmgmtservice:latest

sudo buildctl --addr=nerdctl-container://buildkitd build \
    --frontend dockerfile.v0 \
    --opt filename=Dockerfile \
    --local dockerfile=. \
    --local context=. \
    --output type=oci,name=modelmgmtservice:latest | sudo nerdctl load --namespace k8s.io

nerdctl images --namespace k8s.io modelmgmtservice

kubectl scale deployment -n traininghost modelmgmtservice --replicas 0
kubectl scale deployment -n traininghost modelmgmtservice --replicas 1

sleep 5
curl localhost:32006/getModelInfoList

해당 code는 오픈소스에 올리지 않고 test하기 편하기 위해서만 만들었다.

다음으로 이제 실행하여 결과를 보도록 하자.
원하는 모습은 다음과 같다.

{
  "code": 200,
  "message": [
    {
      "name": "mlpipeline",
      "data": ""
    },
    {
      "name": "qoe1",
      "data": "{\"model-name\":\"qoe1\",\"rapp-id\":\"rapp_1\",\"meta-info\":{\"accuracy\":\"90\",\"feature-list\":[\"pdcpBytesDl\",\"pdcpBytesUl\"],\"model-type\":\"timeseries\"}}"
    },
    {
      "name": "qoe3",
      "data": "{\"model-name\":\"qoe3\",\"rapp-id\":\"rapp_4\",\"meta-info\":{\"accuracy\":\"90\",\"feature-list\":[\"pdcpBytesDl\",\"pdcpBytesUl\"],\"model-type\":\"timeseries\"}}"
    }
  ]
}

다음과 같이 현재 model 정보들을 모두 반환하는 code를 만들어보도록 하자.

성공하였다! 다음으로 모델을 새로 등록한 뒤에 확인해보도록 하자.

curl --location 'http://localhost:32006/registerModel' \
      --header 'Content-Type: application/json' \
      --data '{
         "model-name":"gyu",
         "rapp-id": "rapp_gyu",
         "meta-info" :
         {
             "accuracy":"99",
             "model-type":"timeseries",
             "feature-list":["pdcpBytesDl","pdcpBytesUl"]
         }
      }' -v

...
{"code":201,"message":"Model registered successfully"}

gyu라는 가짜 model 정보를 등록하였다. 이제 모든 model 정보 반환 API에서 잘 나오는 지 확인해보도록 하자.

{
  "code": 200,
  "message": [
    {
      "name": "gyu",
      "data": "{\"model-name\":\"gyu\",\"rapp-id\":\"rapp_gyu\",\"meta-info\":{\"accuracy\":\"99\",\"feature-list\":[\"pdcpBytesDl\",\"pdcpBytesUl\"],\"model-type\":\"timeseries\"}}"
    },
    {
      "name": "mlpipeline",
      "data": ""
    },
    {
      "name": "qoe1",
      "data": "{\"model-name\":\"qoe1\",\"rapp-id\":\"rapp_1\",\"meta-info\":{\"accuracy\":\"90\",\"feature-list\":[\"pdcpBytesDl\",\"pdcpBytesUl\"],\"model-type\":\"timeseries\"}}"
    },
    {
      "name": "qoe3",
      "data": "{\"model-name\":\"qoe3\",\"rapp-id\":\"rapp_4\",\"meta-info\":{\"accuracy\":\"90\",\"feature-list\":[\"pdcpBytesDl\",\"pdcpBytesUl\"],\"model-type\":\"timeseries\"}}"
    }
  ]
}

무려 gyu모델이 추가되고 data정보도 일치하는 것을 볼 수 있다.

test code도 생성하여 검증을 완료하도록 하자.

  • apis_test/mmes_apis_test.go
func TestWhenSuccessGetModelInfoList(t *testing.T) {
	// Setting ENV
	os.Setenv("LOG_FILE_NAME", "testing")

	// Setting Mock
	dbMgrMockInst := new(dbMgrMock)
	dbMgrMockInst.On("ListBucket").Return([]core.ModelInfo{
		{
			Name: "qoe",
			Data: registerModelBody,
		},
	}, nil)

	handler := apis.NewMmeApiHandler(dbMgrMockInst)
	router := routers.InitRouter(handler)
	responseRecorder := httptest.NewRecorder()

	req, _ := http.NewRequest("GET", "/getModelInfoList", nil)
	router.ServeHTTP(responseRecorder, req)

	response := responseRecorder.Result()
	body, _ := io.ReadAll(response.Body)

	var modelInfoListResp struct {
		Code    int                           `json:"code"`
		Message []apis.ModelInfoResponseModel `json:"message"`
	}
	json.Unmarshal(body, &modelInfoListResp)

	assert.Equal(t, 200, responseRecorder.Code)
	assert.Equal(t, 200, modelInfoListResp.Code)
	assert.Equal(t, registerModelBody, modelInfoListResp.Message[0].Data)
}

func TestWhenFailGetModelInfoList(t *testing.T) {
	// Setting ENV
	os.Setenv("LOG_FILE_NAME", "testing")

	// Setting Mock
	dbMgrMockInst := new(dbMgrMock)
	dbMgrMockInst.On("ListBucket").Return([]core.ModelInfo{}, errors.New("Test: Fail GetModelInfoList"))

	handler := apis.NewMmeApiHandler(dbMgrMockInst)
	router := routers.InitRouter(handler)
	responseRecorder := httptest.NewRecorder()

	req, _ := http.NewRequest("GET", "/getModelInfoList", nil)
	router.ServeHTTP(responseRecorder, req)

	response := responseRecorder.Result()
	body, _ := io.ReadAll(response.Body)

	var modelInfoListResp struct {
		Code    int                           `json:"code"`
		Message []apis.ModelInfoResponseModel `json:"message"`
	}
	json.Unmarshal(body, &modelInfoListResp)

	assert.Equal(t, 500, responseRecorder.Code)
	assert.Equal(t, 500, modelInfoListResp.Code)
	assert.Equal(t, 0, len(modelInfoListResp.Message))
}

test code를 실행해보도록 하자.

LOG_FILE_NAME=./testing.log go test ./apis_test/mmes_apis_test.go
ok      command-line-arguments  0.007s

test code도 성공하였다.

이제 jira에 review를 올려보도록 하자.
https://lf-o-ran-sc.atlassian.net/jira/software/c/projects/AIMLFW/issues

생성한 issue는 아래와 같다.
https://lf-o-ran-sc.atlassian.net/jira/software/c/projects/AIMLFW/issues/AIMLFW-128?jql=project%20%3D%20%22AIMLFW%22%20ORDER%20BY%20created%20DESC

이제 gerrit에 올려놓고, review를 지켜보도록 하자.
https://gerrit.o-ran-sc.org/r/c/aiml-fw/awmf/modelmgmtservice/+/13259

하다보니까 modelmgmtservice에서 앞으로 정리해야할 일들이 많아보인다.
1. test 방식 정형화하기
2. golang스러운 test 방법 적용하기
3. logging tool 수정
4. 각 domain마다의 model 객체 생성하기

그렇게 일주일 동안 리뷰가 이어졌고... 간신히 코드는 넣었지만 기본 구조부터 다 바꾸기로 했다...
https://gerrit.o-ran-sc.org/r/c/aiml-fw/awmf/modelmgmtservice/+/13259

0개의 댓글