Ginで実装しているAPIにバックグラウンドで実行するタスクを追加したくなったので、 小さな構成を作成して、挙動を確認することにした。

仕様

要求

  1. TCP 8000でポートを開きHTTPサーバを稼動させる。

  2. HTTPサーバは GET / に対して特定の文字列を返す。

  3. 2)で返すBODYはS3互換オブジェクトストレージのexampleバケットのobject.txtキーで取得できるファイルの中身とする。

  4. 2)で返すデータは1分間キャッシュする。またキャッシュはアプリケーションで行う。

  5. HTTPサーバはSIGTERMを受信すると5秒以内に停止する。

制限

  1. 言語はGolang 1.17を使用する。

  2. HTTPサーバーのためのアプリケーションフレームワークとしてGinを使用する。

設計

システム構成

https://res.cloudinary.com/symdon/image/upload/v1652413591/blog.symdon.info/1652408244/sysmte-architecture_dhtaaf.png

各種コンポーネント

https://res.cloudinary.com/symdon/image/upload/v1652413591/blog.symdon.info/1652408244/component-architecture_fbsb4b.png

アクティビティ

HTTP Serverが起動した

  1. MinIOのexampleバケットからobject.txtを取得する。

  2. 取得したobject.txtのデータをCached Valueにコピーする。

  3. バックグラウンドジョブを開始する。

  4. APIサーバーを設定する。

  5. APIサーバーを起動する。

clientが GET / を受信した

  1. リクエストを受信する。

  2. Cached Valueを用いてResponseを生成する。

  3. 生成したResponseを返す。

HTTP Serverのバックグラウンドジョブが起動してから1分立った

  1. 1分立ったイベントを受信する。

  2. MinIOのexampleバケットからobject.txtを取得する。

  3. 取得したobject.txtのデータをCached Valueにコピーする。

HTTP Serverのバックグラウンドジョブ中にエラーが発生した

  1. エラーの発生をメインスレッドに通知する。

  2. メインスレッドを異常終了する。

HTTP ServerがSIG TERMを受信した

  1. バックグラウンドジョブを停止する。

  2. メインスレッドを停止する。

実装

main.go::

package main

import (
	"time"
	"io"
	"io/ioutil"
	"bufio"
	// "bytes"
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	aws_session "github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/gin-gonic/gin"
	"github.com/rs/zerolog/log"
)

var CachedValue string


func FetchNewCachedValueFromS3 (input *s3.GetObjectInput, awsConfig *aws.Config) (string, error) {
	fp, err := ioutil.TempFile("", "example")
	defer fp.Close()
	sess := aws_session.New(awsConfig)
	downloader := s3manager.NewDownloader(sess)
	downloadedByteSize, err := downloader.Download(fp, input)
	if err != nil {
		panic(err)
	}
	log.Debug().Int64("downloadedByteSize", downloadedByteSize).Msg("Downloaded response data from S3")
	readSeeker := io.ReadSeeker(fp)
	offset, err := readSeeker.Seek(0, 0)
	if err != nil {
		panic(err)
	}
	log.Debug().Int64("offset", offset).Msg("Seeked data")
	if offset != 0 {
		log.Fatal().Msg("Failed to seek")
		panic("Seek error")
	}
	reader := bufio.NewReader(fp)
	line, _, _ := reader.ReadLine()
	return string(line), nil 
}


func main() {
	awsRegion := "ap-northeast-1"
	awsBucketName := "example"
	awsObjectKey := "testing/foo/bar"
	awsOrigin := "http://localhost:9000"

	fmt.Println("Ok")

	awsConfig := &aws.Config{
		Region: aws.String(awsRegion),
	}
	if awsOrigin != "" {
		awsConfig.Endpoint = aws.String(awsOrigin)
		awsConfig.S3ForcePathStyle = aws.Bool(true)
	}
	input := &s3.GetObjectInput{
		Bucket: aws.String(awsBucketName),
		Key:    aws.String(awsObjectKey),
	}
	newValue, err := FetchNewCachedValueFromS3(input, awsConfig)
	if err != nil {
		panic("Failed to fetch data")
	}
	CachedValue = newValue

	r := gin.Default()
	r.GET("/", func (c *gin.Context) {
		c.String(200, CachedValue)
		return
	})

	ticker := time.NewTicker(3 * time.Second)
	stop := make(chan bool)
	go func() {
	loop:
		for {
			select {
			case t := <-ticker.C:
				fmt.Println("Tick at", t)
				newValue, err := FetchNewCachedValueFromS3(input, awsConfig)
				if err != nil {
					panic("Failed to fetch data")
				}
				CachedValue = newValue
			case <-stop:
				break loop
			}
		}
		fmt.Println("Reachable!")
	}()	
	r.Run("127.0.0.1:8000")
	
}

go.mod::

module github.com/TakesxiSximada/symdon/pages/posts/1652408244

go 1.17

require (
	github.com/aws/aws-sdk-go v1.44.13
	github.com/gin-gonic/gin v1.7.7
	github.com/rs/zerolog v1.26.1
)

require (
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/go-playground/locales v0.13.0 // indirect
	github.com/go-playground/universal-translator v0.17.0 // indirect
	github.com/go-playground/validator/v10 v10.4.1 // indirect
	github.com/golang/protobuf v1.3.3 // indirect
	github.com/jmespath/go-jmespath v0.4.0 // indirect
	github.com/json-iterator/go v1.1.9 // indirect
	github.com/leodido/go-urn v1.2.0 // indirect
	github.com/mattn/go-isatty v0.0.12 // indirect
	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
	github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
	github.com/ugorji/go/codec v1.1.7 // indirect
	golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e // indirect
	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
	gopkg.in/yaml.v2 v2.2.8 // indirect
)