--- 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=蒼時弦や]