---
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 基於運算時間計價,並且以帳號為計算。
> 請根據以下的驗收文件,實作對應的功能
### 模型

### 詞彙表
| 單詞 | 解釋
|-----|-----
| 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=蒼時弦や]