---
# System prepended metadata

title: 2023-12-17 - Rails 開發者的進階實戰
tags: [Rails 開發者的進階實戰, Rails Develoepr Foundation]

---

---
tags:
 - Rails Develoepr Foundation
 - Rails 開發者的進階實戰
---
# 2023-12-17 - Rails 開發者的進階實戰

## 事前準備

考慮到練習的時間，課程基本上會以 Mob Programming 的形式進行，仍建議準備好自己的開發環境方便未來練習。

### 開發環境

請以能夠運行這個 [GitHub Repository](https://github.com/StarPortal/rails-developer-foundation-template) 的前提下安裝環境，相關的安裝說明可以參考 README 的內容。

| 環境        | 版本 |
|------------|------|
| Ruby       | 3.1+ |
| PostgreSQL | 14+  |
| Redis      | 7.2+ |
| Node.js    | 18+  |

#### 命令

請確認可以確運行以下命令

| 命令                    | 說明
|------------------------|----------------------------
| `bundle exec cucumber` | 驗收測試用，本次課程只會使用這個
| `bundle exec rspec`    | 單元測試用
| `./bin/dev`            | Rails 伺服器

### 工具

上課過程會使用以下工具

| 名稱              | 說明                             |
|------------------|---------------------------------|
| FigJam           | 需求分析練習，請確認瀏覽器可以正常開啟 |
| JetBrains Client | 安裝連結上課當天公布                |
| 習慣的編輯器       | VSCode、Vim 等皆可                |

## 課後問卷

麻煩大家協助填寫改善課程品質，以及了解大家對哪些類型的內容更有興趣

[➡️ 問卷連結](https://www.surveycake.com/s/gOaGa)

## 講師資訊

課程內容無法涵蓋的部分，可以追蹤網誌、YouTube 會以小單元或者系列連載的方式分享出來。

| 網站 | 介紹 |
|-----------------|------|
| [個人網誌](https://blog.aotoki.me) | 系列文章連載、主題式討論
| [Facebook 粉絲專頁](https://fb.me/aotoki.me) | 主題式討論
| [YouTube 頻道](https://www.youtube.com/channel/UCcABbJfCL0DfNh3wDk_-7lg) | 技術講解
| [Discord 社群](https://discord.com/invite/t2Kd6PNvvA) | 技術討論，上課的問題也可以在此發問
| [訂閱電子報](https://mailchi.mp/aotoki/rails-developer-foundation) | 即時收到網誌通知

## 實作練習

隨著人工智慧的發展，我們已經開發好 AI 功能的 API。然而，我們需要對這個 API 基於運算時間計價，並且以帳號為計算。

> 請根據以下的驗收文件，實作對應的功能

### 模型

![Rails Developer Foundation - 圖表](https://hackmd.io/_uploads/SyxVJ6gS6.jpg)


### 詞彙表

| 單詞 | 解釋
|-----|-----
| Account | 帳號（or User）
| Access Token | 存取令牌
| Usage | 使用量
| Meter | 計量
| Measure | 測量
| Metric  | 指標
| Service | 服務

### 用量查詢（ㄧ）

:::info
試著在不使用資料庫的狀況下完成這個功能 ~ 15m
:::

```gherkin=
#language:zh-TW

功能: 用量查詢
  背景:
      假設 有一些使用者
        | id                                   | name |
        | eb33e167-c381-466e-95ce-6fbc65d3445d | 蒼時  |
      並且 有一些存取令牌
        | id                                   | owner_id                             | token                            |
        | 7644b14e-d0f9-4056-8587-6587bad26d1c | eb33e167-c381-466e-95ce-6fbc65d3445d | yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu |

    場景: 使用者可以查詢用量
        假設 有一些用量紀錄
            | id                                   | created_at          | owner_id                             | usage_amount | usage_type | unit_price |
            | e0fb11cf-170c-4911-ab4f-5c1053339a83 | 2023-11-24 20:00:00 | eb33e167-c381-466e-95ce-6fbc65d3445d | 2100         | cpu_time   | 1          |
            | 9b59cfb9-76d1-4806-b2ec-f01d394f8abe | 2023-11-24 20:05:00 | eb33e167-c381-466e-95ce-6fbc65d3445d | 100          | cpu_time   | 1          |
        當 以 GET 方式呼叫 "/api/usage?token=yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu"
        那麼 可以看到以下 JSON 回應
        """
        {
            "account_name": "蒼時",
            "usage": [
                { "date": "2023-11-24", "cpu_time": 2200, "cost": 2200 }
            ]
        }
        """
```

```ruby=
# features/step_definitions/

Given('有一些使用者') do |table|
  table.hashes.each do |user|
    User.create!(user)
  end
end

Given('有一些存取令牌') do |table|
  table.hashes.each do |access_token|
    AccessToken.create!(access_token)
  end
end

Given('有一些用量紀錄') do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  table.hashes.each do |meter|
    MeterEvent.create!(meter)
  end
end

When('以 GET 方式呼叫 {string}') do |path|
  @response = get path
end

Then('可以看到以下 JSON 回應') do |doc_string|
  expected = JSON.parse(doc_string)
  actual = JSON.parse(@response.body)
  expect(actual).to eq(expected)
end
```

### CPU 費用

:::info
在測試環境下，將測量跟呼叫 AI 模型切換為測試用的版本進行模擬 ~ 30m
:::

:::info
可以使用[程式碼](https://gist.github.com/elct9620/5cc24515093af7c3b284ebdbe96035c3)作為測量運算時間的實作
:::


```rub=
# 啟用 PostgreSQL UUID 功能
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')

# 設定資料表使用 UUID 作為 ID
create_table :users, id: :uuid do |t|
 # ...
end
```

```gherkin=
#language:zh-TW

功能: CPU 計費
  背景:
      假設 有一些使用者
        | id                                   | name |
        | eb33e167-c381-466e-95ce-6fbc65d3445d | 蒼時  |
      並且 有一些存取令牌
        | id                                   | owner_id                             | token                            |
        | 7644b14e-d0f9-4056-8587-6587bad26d1c | eb33e167-c381-466e-95ce-6fbc65d3445d | yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu |

    場景: 使用者花費 2 秒運算
        假設 CPU 運算耗費 2 秒
        並且 CPU 單位價格為 1 元
        當 以 POST 方式呼叫 "/api/brain?token=yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu" 包含以下內容
        """
        {
            "input": {
                "message": "你好"
            }
        }
        """
        並且 以 GET 方式呼叫 "/api/usage?token=yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu"
        那麼 會有 1 筆用量紀錄
        並且 用量報告中 CPU 使用時間為 2000
        並且 用量報告中花費為 2000
```

```ruby=
When('以 POST 方式呼叫 {string} 包含以下內容') do |path, body|
  @response = post path, params: JSON.parse(body)
end

Given('CPU 運算耗費 {int} 秒') do |amount|
  Measure::Fake.time = amount * 1000
end

Given('CPU 單位價格為 {int} 元') do |amount|
  Measure::Fake.price = amount
end

Then('會有 {int} 筆用量紀錄') do |expected|
  data = JSON.parse(@response.body)
  actual = data.fetch('usage', []).size
  expect(actual).to eq(expected)
end

Then('用量報告中 CPU 使用時間為 {int}') do |cpu_time|
  data = JSON.parse(@response.body)
  usage = data.fetch('usage', [])

  expect(usage).to include(include('cpu_time' => cpu_time))
end

Then('用量報告中花費為 {int}') do |cost|
  data = JSON.parse(@response.body)
  usage = data.fetch('usage', [])

  expect(usage).to include(include('cost' => cost))
end
```

### 用量查詢（二）

:::info
將用量轉換成統一的計算單位 ~ 15m
:::

```gherkin=
#language:zh-TW

功能: 用量查詢
  背景:
      假設 有一些使用者
        | id                                   | name |
        | eb33e167-c381-466e-95ce-6fbc65d3445d | 蒼時  |
      並且 有一些存取令牌
        | id                                   | owner_id                             | token                            |
        | 7644b14e-d0f9-4056-8587-6587bad26d1c | eb33e167-c381-466e-95ce-6fbc65d3445d | yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu |

    # ...
    
    場景: 使用者可以查詢 CPU 和 GPU 的總花費
        假設 有一些用量紀錄
            | id                                   | created_at          | owner_id                             | usage_amount | usage_type | unit_price |
            | e0fb11cf-170c-4911-ab4f-5c1053339a83 | 2023-11-22 20:00:00 | eb33e167-c381-466e-95ce-6fbc65d3445d | 1100         | cpu_time   | 1          |
            | 9b59cfb9-76d1-4806-b2ec-f01d394f8abe | 2023-11-24 10:05:00 | eb33e167-c381-466e-95ce-6fbc65d3445d | 100          | gpu_time   | 10         |
        當 以 GET 方式呼叫 "/api/usage?token=yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu"
        那麼 可以看到以下 JSON 回應
        """
        {
            "account_name": "蒼時",
            "usage": [
                { "date": "2023-11-22", "cpu_time": 1100, "gpu_time": 0, "cost": 1100 },
                { "date": "2023-11-24", "cpu_time": 0, "gpu_time": 100, "cost": 1000 }
            ]
        }
        """
```

### GPU 費用

:::info
這是一個非同步處理，需要在 ActionJob 中處理 ~ 30m
:::

:::warning
你會需要回傳一個 ID 並且可以透過 ID 確認查詢運算狀態
:::

```gherkin=
#language:zh-TW

功能: GPU 計費
  背景:
      假設 有一些使用者
        | id                                   | name |
        | eb33e167-c381-466e-95ce-6fbc65d3445d | 蒼時  |
      並且 有一些存取令牌
        | id                                   | owner_id                             | token                            |
        | 7644b14e-d0f9-4056-8587-6587bad26d1c | eb33e167-c381-466e-95ce-6fbc65d3445d | yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu |

    場景: 使用者花費 10 秒運算
        假設 GPU 運算耗費 10 秒
        並且 GPU 單位價格為 10 元
        當 以 POST 方式呼叫 "/api/eyes?token=yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu" 包含以下內容
        """
        {
            "input": {
                "prompt": "一隻橘貓"
            }
        }
        """
        # 從前一個步驟抓取 ID 接續動作
        並且 以存取令牌 "yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu" 等待 GPU 運算完成
        並且 以 GET 方式呼叫 "/api/usage?token=yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu"
        那麼 會有 1 筆用量紀錄
        並且 用量報告中 GPU 使用時間為 10000
        並且 用量報告中花費為 100000
```

```ruby=
Given('GPU 運算耗費 {int} 秒') do |amount|
  Measure::Fake.time = amount * 1000
end

Given('GPU 單位價格為 {int} 元') do |amount|
  Measure::Fake.price = amount
end

When('以存取令牌 {string} 等待 GPU 運算完成') do |token|
  id = JSON.parse(@response.body).fetch('id')

  Timeout.timeout(10) do
    res = get "/api/eyes/#{id}?token=#{token}"
    break if res.status == 200

    sleep 1
  end
end

Then('用量報告中 GPU 使用時間為 {int}') do |cpu_time|
  data = JSON.parse(@response.body)
  usage = data.fetch('usage', [])

  expect(usage).to include(include('gpu_time' => cpu_time))
end
```

### 資料驗證

:::info
區分驗證的機制屬於哪種類型
:::

```gherkin=
#language:zh-TW

功能: CPU 計費
  背景:
      假設 有一些使用者
        | id                                   | name |
        | eb33e167-c381-466e-95ce-6fbc65d3445d | 蒼時  |
      並且 有一些存取令牌
        | id                                   | owner_id                             | token                            |
        | 7644b14e-d0f9-4056-8587-6587bad26d1c | eb33e167-c381-466e-95ce-6fbc65d3445d | yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu |


    # ...
    場景: 使用者提供了錯誤的訊息
        當 以 POST 方式呼叫 "/api/brian?token=yC63BMuCNqkVCxCRAKKPcWAMe7qxxmbu" 包含以下內容
        """
        {
            "input": {
                "prompt": "一隻橘貓"
            }
        }
        """
        那麼 可以看到以下 JSON 回應
        """
        {
            "error": "message 不能為空"
        }
        """
```

### 加速查詢（延伸討論）

:::info
當大量呼叫時會有非常多的測量事件，呈現用量會變得非常慢，該如何改善？
:::

## 共筆區域

> 歡迎在這裡撰寫筆記跟其他同學協力紀錄
> [name=蒼時弦や]