203 lines
7.1 KiB
Go
203 lines
7.1 KiB
Go
package environment
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// GatewayTaskStatus 是任务生命周期;dispatched 后连接中断标记 failed,不自动重发。
|
|
const (
|
|
TaskPending = "pending"
|
|
TaskDispatched = "dispatched"
|
|
TaskSucceeded = "succeeded"
|
|
TaskFailed = "failed"
|
|
)
|
|
|
|
// GatewayTask 是一次平台→网关通信的持久化记录,全量任务即执行记录。
|
|
type GatewayTask struct {
|
|
ID int64 `json:"id"`
|
|
GatewayID int64 `json:"gateway_id"`
|
|
Method string `json:"method"`
|
|
Path string `json:"path"`
|
|
Payload json.RawMessage `json:"payload"`
|
|
Status string `json:"status"`
|
|
ResponseStatus int `json:"response_status"`
|
|
Error string `json:"error"`
|
|
ResponseBytes int64 `json:"response_bytes"`
|
|
TimeoutSeconds int `json:"timeout_seconds"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
DispatchedAt *time.Time `json:"dispatched_at"`
|
|
FinishedAt *time.Time `json:"finished_at"`
|
|
}
|
|
|
|
func validGatewayMethod(method string) bool {
|
|
switch method {
|
|
case "GET", "POST", "PUT", "DELETE":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
const gatewayTaskColumns = `id, gateway_id, method, path, payload, status, response_status, error, response_bytes, timeout_seconds, created_at, dispatched_at, finished_at`
|
|
|
|
func scanGatewayTask(row rowScanner) (GatewayTask, error) {
|
|
var task GatewayTask
|
|
var dispatched, finished sql.NullTime
|
|
if err := row.Scan(&task.ID, &task.GatewayID, &task.Method, &task.Path, &task.Payload, &task.Status,
|
|
&task.ResponseStatus, &task.Error, &task.ResponseBytes, &task.TimeoutSeconds,
|
|
&task.CreatedAt, &dispatched, &finished); err != nil {
|
|
return GatewayTask{}, rowError(err)
|
|
}
|
|
if dispatched.Valid {
|
|
task.DispatchedAt = &dispatched.Time
|
|
}
|
|
if finished.Valid {
|
|
task.FinishedAt = &finished.Time
|
|
}
|
|
return task, nil
|
|
}
|
|
|
|
// CreateGatewayTask 入队一个任务;payload 必须是合法 JSON。
|
|
func (s *Store) CreateGatewayTask(ctx context.Context, gatewayID int64, method, path string, payload []byte, timeout time.Duration) (GatewayTask, error) {
|
|
if gatewayID <= 0 || !validGatewayMethod(method) || path == "" || len(path) > 512 || !json.Valid(payload) {
|
|
return GatewayTask{}, ErrInvalid
|
|
}
|
|
seconds := int(timeout / time.Second)
|
|
if seconds < 1 || seconds > 600 {
|
|
return GatewayTask{}, ErrInvalid
|
|
}
|
|
if len(payload) == 0 {
|
|
payload = []byte(`{}`)
|
|
}
|
|
return scanGatewayTask(s.db.QueryRowContext(ctx, `
|
|
INSERT INTO gateway_task (gateway_id, method, path, payload, timeout_seconds)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING `+gatewayTaskColumns, gatewayID, method, path, payload, seconds))
|
|
}
|
|
|
|
// DispatchGatewayTask 抢占一个待下发任务;仅当仍为 pending 时成功,保证单次下发。
|
|
func (s *Store) DispatchGatewayTask(ctx context.Context, taskID int64) (bool, error) {
|
|
result, err := s.db.ExecContext(ctx, `
|
|
UPDATE gateway_task SET status = 'dispatched', dispatched_at = now()
|
|
WHERE id = $1 AND status = 'pending'`, taskID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
count, err := result.RowsAffected()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count == 1, nil
|
|
}
|
|
|
|
// FinishGatewayTask 记录任务终态;dispatched 中任务由结果更新,pending 任务允许直接终态(网关离线),重复投递幂等忽略。
|
|
func (s *Store) FinishGatewayTask(ctx context.Context, taskID int64, status string, responseStatus int, taskError string, responseBytes int64) (bool, error) {
|
|
if status != TaskSucceeded && status != TaskFailed {
|
|
return false, ErrInvalid
|
|
}
|
|
if len(taskError) > 2048 {
|
|
taskError = taskError[:2048]
|
|
}
|
|
result, err := s.db.ExecContext(ctx, `
|
|
UPDATE gateway_task SET status = $2, response_status = $3, error = $4, response_bytes = $5, finished_at = now()
|
|
WHERE id = $1 AND status IN ('dispatched', 'pending')`, taskID, status, responseStatus, taskError, responseBytes)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
count, err := result.RowsAffected()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count == 1, nil
|
|
}
|
|
|
|
// InterruptDispatchedTasks 连接断开时把该网关在途任务标记失败(结果未确认,不自动重发)。
|
|
func (s *Store) InterruptDispatchedTasks(ctx context.Context, gatewayID int64, reason string) error {
|
|
if len(reason) > 2048 {
|
|
reason = reason[:2048]
|
|
}
|
|
_, err := s.db.ExecContext(ctx, `
|
|
UPDATE gateway_task SET status = 'failed', error = $2, finished_at = now()
|
|
WHERE gateway_id = $1 AND status = 'dispatched'`, gatewayID, reason)
|
|
return err
|
|
}
|
|
|
|
// ResetGatewayConnections 平台启动时清空内存会话遗留的在线标记。
|
|
func (s *Store) ResetGatewayConnections(ctx context.Context) error {
|
|
_, err := s.db.ExecContext(ctx, `UPDATE gateway SET online = false, version = '' WHERE online`)
|
|
return err
|
|
}
|
|
|
|
// NextPendingGatewayTasks 读取某网关按 id 序的待下发任务(补发队列)。
|
|
func (s *Store) NextPendingGatewayTasks(ctx context.Context, gatewayID int64, limit int) ([]GatewayTask, error) {
|
|
if gatewayID <= 0 || limit <= 0 {
|
|
return nil, ErrInvalid
|
|
}
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT `+gatewayTaskColumns+` FROM gateway_task
|
|
WHERE gateway_id = $1 AND status = 'pending' ORDER BY id LIMIT $2`, gatewayID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
tasks := []GatewayTask{}
|
|
for rows.Next() {
|
|
task, err := scanGatewayTask(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tasks = append(tasks, task)
|
|
}
|
|
return tasks, rows.Err()
|
|
}
|
|
|
|
type GatewayTaskPage struct {
|
|
Data []GatewayTask `json:"data"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
// ListGatewayTasksPage 分页读取网关执行记录,最新创建在前。
|
|
func (s *Store) ListGatewayTasksPage(ctx context.Context, gatewayID int64, page, pageSize int) (GatewayTaskPage, error) {
|
|
if gatewayID <= 0 {
|
|
return GatewayTaskPage{}, ErrInvalid
|
|
}
|
|
if page < 1 || pageSize < 1 || pageSize > 100 {
|
|
return GatewayTaskPage{}, ErrInvalid
|
|
}
|
|
result := GatewayTaskPage{Data: []GatewayTask{}, Page: page, PageSize: pageSize}
|
|
if err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM gateway_task WHERE gateway_id = $1`, gatewayID).Scan(&result.Total); err != nil {
|
|
return result, err
|
|
}
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT `+gatewayTaskColumns+` FROM gateway_task WHERE gateway_id = $1
|
|
ORDER BY id DESC LIMIT $2 OFFSET $3`, gatewayID, pageSize, (page-1)*pageSize)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
task, err := scanGatewayTask(rows)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Data = append(result.Data, task)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// CountPendingGatewayTasks 返回各网关待执行任务数(队列状态展示)。
|
|
func (s *Store) CountPendingGatewayTasks(ctx context.Context, gatewayID int64) (int, error) {
|
|
var count int
|
|
if err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM gateway_task WHERE gateway_id = $1 AND status = 'pending'`, gatewayID).Scan(&count); err != nil {
|
|
return 0, err
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
var ErrGatewayOffline = errors.New("网关离线,无法执行任务")
|