From ef924fb0737c07f0d89a5c474154507068447c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Fri, 27 Dec 2024 01:01:30 +0800 Subject: [PATCH] feat: add readme --- .idea/vcs.xml | 6 + Makefile | 7 +- build/Dockerfile | 47 ++++++++ docker-compose.yml | 31 +++++ etc/url.yaml | 4 +- internal/module/url/mock/repository/url.go | 10 ++ internal/module/url/repository/url.go | 5 - internal/module/url/repository/url_test.go | 35 ++---- .../module/url/usecase/short_code_utils.go | 1 + .../url/usecase/short_code_utils_test.go | 72 ++++++++++++ internal/svc/service_context.go | 5 + readme.md | 111 ++++++++++++++++++ 12 files changed, 297 insertions(+), 37 deletions(-) create mode 100644 .idea/vcs.xml create mode 100644 build/Dockerfile create mode 100644 docker-compose.yml create mode 100644 internal/module/url/mock/repository/url.go create mode 100644 internal/module/url/usecase/short_code_utils_test.go create mode 100644 readme.md diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile index 9cd4db7..dcdbbec 100755 --- a/Makefile +++ b/Makefile @@ -32,10 +32,5 @@ gen-api: # 產生 api .PHONY: mock-gen mock-gen: # 建立 mock 資料 - mockgen -source=./internal/module/product/domain/repository/category.go -destination=./internal/module/product/mock/repository/category.go -package=mock - mockgen -source=./internal/module/product/domain/repository/supplementary_info.go -destination=./internal/module/product/mock/repository/supplementary_info.go -package=mock - mockgen -source=./internal/module/product/domain/repository/product.go -destination=./internal/module/product/mock/repository/product.go -package=mock - mockgen -source=./internal/module/product/domain/repository/tags.go -destination=./internal/module/product/mock/repository/tags.go -package=mock - mockgen -source=./internal/module/product/domain/repository/product_item.go -destination=./internal/module/product/mock/repository/product_item.go -package=mock - mockgen -source=./internal/module/cart/domain/repository/cart.go -destination=./internal/module/cart/mock/repository/cart.go -package=mock + mockgen -source=./internal/module/url/repository/url.go -destination=./internal/module/url//mock/repository/url.go -package=mock @echo "Generate mock files successfully" diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..c6b8b97 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,47 @@ +########### +# BUILDER # +########### + +FROM golang:1.22.3 as builder + +ARG VERSION +ARG BUILT +ARG GIT_COMMIT +ARG SSH_PRV_KEY + +# private go packages +ENV GOPRIVATE=code.30cm.net +ENV FLAG="-s -w -X main.Version=${VERSION} -X main.Built=${BUILT} -X main.GitCommit=${GIT_COMMIT}" +WORKDIR /app +COPY . . + + +RUN apt-get update && \ + apt-get install git + +# Make the root foler for our ssh +RUN mkdir -p /root/.ssh && \ + chmod 0700 /root/.ssh && \ + ssh-keyscan git.30cm.net > /root/.ssh/known_hosts && \ + echo "$SSH_PRV_KEY" > /root/.ssh/id_rsa && \ + chmod 600 /root/.ssh/id_rsa + + + +RUN --mount=type=ssh go mod download + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "$FLAG" \ + -o url + +########## +## FINAL # +########## +# +FROM gcr.io/distroless/static-debian11 +WORKDIR /app + +COPY --from=builder /app/url /app/url +COPY --from=builder /app/etc/url.yaml /app/etc/url.yaml +EXPOSE 8888 +CMD ["/app/url"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3e2e6cc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: "3.9" + +services: + app: + build: + context: . # 當前目錄為構建上下文 + dockerfile: ./build/Dockerfile + environment: + - VERSION=1.0.0 + - BUILT=$(date +%Y-%m-%d) + - GIT_COMMIT=$(git rev-parse --short HEAD) + - SSH_PRV_KEY=${SSH_PRV_KEY} # 傳遞私鑰,用於私有倉庫 + ports: + - "8888:8888" + depends_on: + - mongo + - redis + volumes: + - ./etc:/app/etc + + mongo: + image: mongo:8.0 + container_name: mongo + ports: + - "27017:27017" + + redis: + image: redis:7.0 + container_name: redis + ports: + - "6379:6379" \ No newline at end of file diff --git a/etc/url.yaml b/etc/url.yaml index 771b167..05c0239 100644 --- a/etc/url.yaml +++ b/etc/url.yaml @@ -7,8 +7,8 @@ Cache: type: node Mongo: - Schema: - Host: + Schema: mongodb + Host: "127.0.0.1:27017" User: Password: Database: digimon_url diff --git a/internal/module/url/mock/repository/url.go b/internal/module/url/mock/repository/url.go new file mode 100644 index 0000000..5f4f4a7 --- /dev/null +++ b/internal/module/url/mock/repository/url.go @@ -0,0 +1,10 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/module/url/repository/url.go +// +// Generated by this command: +// +// mockgen -source=./internal/module/url/repository/url.go -destination=./internal/module/url//mock/repository/url.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock diff --git a/internal/module/url/repository/url.go b/internal/module/url/repository/url.go index 02eb53d..fc3798f 100644 --- a/internal/module/url/repository/url.go +++ b/internal/module/url/repository/url.go @@ -173,8 +173,3 @@ func (repo *URLRepository) Index20241226001UP(ctx context.Context) (*mongo.Curso return repo.DB.GetClient().Indexes().List(ctx) } - -// ptr 是一個幫助函數,用於生成指針 -func ptr[T any](v T) *T { - return &v -} diff --git a/internal/module/url/repository/url_test.go b/internal/module/url/repository/url_test.go index 55dfec7..3cf4e4e 100644 --- a/internal/module/url/repository/url_test.go +++ b/internal/module/url/repository/url_test.go @@ -3,6 +3,7 @@ package repository import ( mgo "code.30cm.net/digimon/library-go/mongo" "context" + "fmt" "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/cache" @@ -22,8 +23,7 @@ func SetupTestURLRepository(db string) (repository.URLRepository, func(), error) conf := &mgo.Conf{ Schema: "mongodb", - Host: h, - Port: p, + Host: fmt.Sprintf("%s:%s", h, p), Database: db, MaxStaleness: 300, MaxPoolSize: 100, @@ -50,9 +50,9 @@ func SetupTestURLRepository(db string) (repository.URLRepository, func(), error) } param := URLRepositoryParam{ - conf: conf, - cacheConf: cacheConf, - cacheOpts: cacheOpts, + Conf: conf, + CacheConf: cacheConf, + CacheOpts: cacheOpts, } repo := NewURLRepository(param) @@ -61,7 +61,7 @@ func SetupTestURLRepository(db string) (repository.URLRepository, func(), error) return repo, tearDown, nil } -func TestAccountModel_Insert(t *testing.T) { +func TestAccountModel_InsertMany(t *testing.T) { repo, tearDown, err := SetupTestURLRepository("testDB") defer tearDown() assert.NoError(t, err) @@ -74,7 +74,7 @@ func TestAccountModel_Insert(t *testing.T) { { name: "Valid account insert", account: &entity.URLTable{ - ShortCode: "123", + ShortCode: "AAAAA", URL: "https://www.google.com", }, expectError: false, @@ -89,23 +89,10 @@ func TestAccountModel_Insert(t *testing.T) { assert.Error(t, err) } else { assert.NoError(t, err, "插入操作應該成功") - - //// 檢查是否生成了 ObjectID 和時間戳 - //assert.NotZero(t, tt.account.ID, "ID 應該被生成") - //assert.NotNil(t, tt.account.CreateAt, "CreateAt 應該被設置") - //assert.NotNil(t, tt.account.UpdateAt, "UpdateAt 應該被設置") - // - //// 檢查插入的時間是否合理 - //now := time.Now().UTC().UnixNano() - //assert.LessOrEqual(t, *tt.account.CreateAt, now, "CreateAt 應在當前時間之前") - //assert.LessOrEqual(t, *tt.account.UpdateAt, now, "UpdateAt 應在當前時間之前") - // - //// 確認插入的資料 - //insertedAccount, err := repo.FindOne(context.Background(), tt.account.ID.Hex()) - //assert.NoError(t, err, "應該可以找到插入的帳號資料") - //assert.Equal(t, tt.account.LoginID, insertedAccount.LoginID, "LoginID 應相同") - //assert.Equal(t, tt.account.Token, insertedAccount.Token, "Token 應相同") - //assert.Equal(t, tt.account.Platform, insertedAccount.Platform, "Platform 應相同") + one, err := repo.FindOne(context.Background(), tt.account.ShortCode) + assert.NoError(t, err) + assert.Equal(t, one.ShortCode, tt.account.ShortCode) + assert.Equal(t, one.URL, tt.account.URL) } }) } diff --git a/internal/module/url/usecase/short_code_utils.go b/internal/module/url/usecase/short_code_utils.go index 2bd4031..715dc61 100644 --- a/internal/module/url/usecase/short_code_utils.go +++ b/internal/module/url/usecase/short_code_utils.go @@ -48,6 +48,7 @@ func (alloc *IDAllocator) Release(id string) error { // 將對應位設置為 0 alloc.bitmap.SetBit(alloc.bitmap, index, 0) + return nil } diff --git a/internal/module/url/usecase/short_code_utils_test.go b/internal/module/url/usecase/short_code_utils_test.go new file mode 100644 index 0000000..8ef0979 --- /dev/null +++ b/internal/module/url/usecase/short_code_utils_test.go @@ -0,0 +1,72 @@ +package usecase + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestIDAllocator(t *testing.T) { + allocator := NewIDAllocator() + + // 測試分配 ID + t.Run("Allocate IDs", func(t *testing.T) { + id1, err := allocator.Allocate() + assert.NoError(t, err) + assert.NotEmpty(t, id1, "Allocated ID should not be empty") + + id2, err := allocator.Allocate() + assert.NoError(t, err) + assert.NotEmpty(t, id2, "Allocated ID should not be empty") + assert.NotEqual(t, id1, id2, "Allocated IDs should be unique") + }) + + // 測試釋放 ID + t.Run("Release ID", func(t *testing.T) { + id, err := allocator.Allocate() + assert.NoError(t, err) + + err = allocator.Release(id) + assert.NoError(t, err, "Releasing an allocated ID should succeed") + + // 重新分配應該得到相同的 ID + newID, err := allocator.Allocate() + assert.NoError(t, err) + assert.Equal(t, id, newID, "Reallocated ID should be the same as the released ID") + }) + + // 測試釋放無效的 ID + t.Run("Release invalid ID", func(t *testing.T) { + err := allocator.Release("錯誤") + assert.Error(t, err, "Releasing an invalid ID should return an error") + }) + + // 測試超過位圖容量的情況 + t.Run("Exceed bitmap capacity", func(t *testing.T) { + // 分配 64 個 ID + for i := 0; i < 61; i++ { + _, err := allocator.Allocate() + assert.NoError(t, err) + } + + // 第 65 次分配應該返回錯誤 + _, err := allocator.Allocate() + assert.Error(t, err, "Allocating more than 64 IDs should fail") + }) +} + +func TestEncodeDecodeIndex(t *testing.T) { + t.Run("Encode and Decode", func(t *testing.T) { + for i := 0; i < 64; i++ { + encoded := encodeIndex(i) + decoded, err := decodeIndex(encoded) + + assert.NoError(t, err) + assert.Equal(t, i, decoded, "Decoded index should match the original") + } + }) + + t.Run("Decode invalid string", func(t *testing.T) { + _, err := decodeIndex("不可以") + assert.Error(t, err, "Decoding an invalid ID should return an error") + }) +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 5ede48c..c17c52f 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -1,6 +1,7 @@ package svc import ( + "context" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/mon" "time" @@ -30,6 +31,10 @@ func NewServiceContext(c config.Config) *ServiceContext { cache.WithNotFoundExpiry(DefaultFindDataNotFoundTimeout), }, }) + _, err := ur.Index20241226001UP(context.Background()) + if err != nil { + panic(err) + } urlUseCase := uc.MustURLUseCase(uc.URLUseCaseParam{ URLRepo: ur, diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..022a9c5 --- /dev/null +++ b/readme.md @@ -0,0 +1,111 @@ + +## 本地端啟動(測試用) + +[//]: # (請先設定資料庫) + +```bash +go mod tidy +# 進入 etc/gateway.yml 設定各依賴的 config +go run url.go +``` + +### 本地啟動需要依賴套件或工具 + +以下已 macOS 為例,若已安裝或有其他可用服務則可跳過 + +- go-zero goctl (https://go-zero.dev/docs/tasks/installation/goctl) + ```bash + $ go install github.com/zeromicro/go-zero/tools/goctl@latest + ``` +- protobuf (https://protobuf.dev/) + ```bash + brew install protobuf + ``` + +## Docker 啟動 (Production) + +```bash +make build-docker +make run-docker +``` + +## 目錄結構 +```aiignore +├── Makefile # 用於構建和運行專案的自動化腳本 +├── etc +│   └── url.yaml # 服務配置檔,例如端口號、資料庫連線等 +├── generate # 產生服務的目錄 +│   └── api +│   └── url_generate.api # API 定義檔,描述服務介面和資料結構 +├── go.mod +├── go.sum +├── internal # 內部模組目錄,不對外暴露 +│   ├── config +│   │   └── config.go # 配置管理邏輯,負責加載和解析配置檔 +│   ├── handler +│   │   ├── routes.go # 路由註冊檔案,將請求分發到對應的處理器 +│   │   └── url +│   │   └── # URL 相關請求處理器目錄,具體實現短網址的創建、查詢等功能 +│   ├── logic +│   │   └── url +│   │   └── # 核心業務邏輯層,實現短網址生成、存儲和讀取 +│   ├── module +│   │   └── url +│   │   └── # URL 子模組的實現,封裝功能細節 +│   ├── svc +│   │   └── service_context.go # 服務上下文,管理全域依賴(如資料庫連線等) +│   └── types +│   └── types.go # 通用型別定義,例如請求和回應的結構體 +├── readme.md # 專案說明檔,描述專案目標、使用方法等 +├── url.go # 服務的入口檔案,負責啟動服務 +└── url.json # 服務元數據或範例配置檔,用於輔助開發或測試 +``` + +## 設計思路 + +IDAllocator 是一個基於 Bitmap的簡單 ID 分配器,用於管理有限數量的 ID。通過位圖來標記 ID 的使用狀態,每個位對應一個唯一的 ID,從而高效地分配和釋放資源。 + +### 核心設計理念 + 1. 使用 math/big 提供的 big.Int 作為位圖,記錄每個 ID 是否已被分配。 + 2. 同步安全:通過 sync.Mutex 確保分配和釋放操作的線程安全性。 + 3. 編碼與解碼:將位圖中的索引轉換為 URL 友好的短碼(使用 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ 作為字符集),以便對外提供友好的短字符串表示。 + +### 功能說明 +#### 分配 ID (Allocate) + * 從位圖中找到第一個未使用的位置,分配對應的 ID。 + * 將該位設為 1 表示已分配,並返回經過編碼的短字符串。 + +#### 釋放 ID (Release) + * 將傳入的 ID 解碼為位圖索引。 + * 將對應位設為 0,表示該 ID 已釋放,可重複使用。 + +#### ID 編碼 (encodeIndex) + * 將位圖索引轉換為固定長度(5 字符)的短碼。 -> **故目前可用會是64 的五次方個(考慮單機架構),已夠用,如果需要分散式可以再討論** + * 確保生成的 ID 短小且唯一。 + +#### ID 解碼 (decodeIndex) + * 將短碼還原為位圖索引,確保釋放或其他操作可以正確定位。 + + +### 限制與考量 + 1. 前可用會是 64 的五次方個(考慮單機架構),已夠用,如果需要分散式可以再討論 + 2. 目前設計並沒有考慮壞掉重啟,只有讓他不斷插入,在資料庫設定為一所以,插入不了就會在產新的,的消極處理方案 + 如果需求需要重啟復原,可以在討論如何做 + + + +### 分布式設計考量 + +#### 如果需要支持分布式環境,需引入全局一致性和協作管理,可能的改進方向包括: + +1. 集中式位圖管理: + * 使用集中式存儲(例如 Redis 或 Zookeeper)存儲位圖數據。 + * 各實例在分配和釋放時,通過原子操作更新集中位圖。 +2. 分段分配: + * 將 ID 空間劃分為多段,分配給不同的節點,每個節點管理自己的位圖。 + * 節點之間通過協議(如 Raft)協作管理段的分配。 +3. 雪花算法(Snowflake Algorithm): + * 基於時間戳、機器 ID 和序列號生成全局唯一的 ID。 + * 適用於大規模分布式系統。 + +時間尚短,測試先只做最基本的short_code_utils \ No newline at end of file