Перейти к содержанию

Хранимые процедуры

Вызов Lua процедур в Octopus через ProcFields*.

Обзор

ProcFields* позволяет вызывать хранимые процедуры Octopus:

sequenceDiagram
    participant App as Приложение
    participant AR as ActiveRecord
    participant Octopus as Octopus

    App->>AR: stats.Call(ctx)
    AR->>Octopus: call "calculate_stats" [userId]
    Octopus-->>AR: [count, total, avg]
    AR-->>App: stats.GetCount(), etc.

Декларация

//ar:serverConf:octconf
//ar:namespace:calculate_stats   // Имя Lua процедуры
//ar:backend:octopus
type ProcFieldsStats struct {
    // Входные параметры
    UserId    string `ar:"input;size:32"`
    StartDate string `ar:"input;size:10"`
    EndDate   string `ar:"input;size:10"`

    // Выходные параметры (порядок = порядок в tuple результата)
    OrderCount  uint32 `ar:"output"`
    TotalAmount int64  `ar:"output"`
    AvgAmount   int64  `ar:"output"`
}

Теги

Тег Описание
input Входной параметр
output Выходной параметр
output:N Выходной с явным порядковым номером
serializer Сериализатор для параметра
size Размер параметра

Использование

// Создание объекта для вызова
stats := stats.New(ctx)

// Установка входных параметров
stats.SetUserId("12345")
stats.SetStartDate("2024-01-01")
stats.SetEndDate("2024-12-31")

// Вызов процедуры
if err := stats.Call(ctx); err != nil {
    return fmt.Errorf("call failed: %w", err)
}

// Чтение результатов
fmt.Printf("Orders: %d\n", stats.GetOrderCount())
fmt.Printf("Total: %d\n", stats.GetTotalAmount())
fmt.Printf("Average: %d\n", stats.GetAvgAmount())

Входные параметры

Только строковые типы

Для Octopus входные параметры могут быть только строкового типа:

type ProcFieldsMyProc struct {
    // ✅ Правильно
    UserId string `ar:"input;size:32"`
    Data   []byte `ar:"input;size:1024"`

    // ❌ Неправильно — не строковый тип
    // Count int `ar:"input"`
}

Сложные типы через сериализатор

type ProcFieldsMyProc struct {
    // Сериализатор преобразует структуру в строку
    Params string `ar:"input;serializer:Json;size:2048"`
}

type SerializersMyProc struct {
    Params *MyParamsStruct `ar:"pkg:github.com/myapp/types;object:ParamsS"`
}

Списки через сериализатор

type ProcFieldsMyProc struct {
    Tags string `ar:"input;serializer:TagList;size:512"`
}

type SerializersMyProc struct {
    Tags []string `ar:"pkg:github.com/myapp/serializers;object:TagListS"`
}

Сериализатор возвращает []string — каждый элемент станет отдельным параметром:

func TagListSMarshal(tags []string) ([]string, error) {
    return tags, nil  // ["tag1", "tag2"] → отдельные параметры
}

Выходные параметры

Порядок полей

Порядок объявления output полей должен соответствовать порядку в tuple результата:

type ProcFieldsStats struct {
    // Tuple результата: [count, total, avg]
    OrderCount  uint32 `ar:"output"`  // tuple[0]
    TotalAmount int64  `ar:"output"`  // tuple[1]
    AvgAmount   int64  `ar:"output"`  // tuple[2]
}

Явный порядковый номер

type ProcFieldsStats struct {
    // Можно указать порядок явно
    AvgAmount   int64  `ar:"output:2"`
    TotalAmount int64  `ar:"output:1"`
    OrderCount  uint32 `ar:"output:0"`
}

Неполный результат

Если tuple содержит меньше полей, оставшиеся будут пустыми:

// Ожидаем 3 поля, пришло 2
stats.GetOrderCount()   // Значение из tuple[0]
stats.GetTotalAmount()  // Значение из tuple[1]
stats.GetAvgAmount()    // 0 (по умолчанию)

Пример Lua процедуры

-- В Octopus
box.schema.func.create('calculate_stats', {
    language = 'LUA',
    body = [[
        function(user_id, start_date, end_date)
            local count = 0
            local total = 0

            -- Логика расчёта...

            return {count, total, total / count}
        end
    ]]
})

Сериализаторы для параметров

Входные параметры

Сериализатор для входных параметров должен возвращать string или []string:

// Один параметр
func ParamsMarshal(p *Params) (string, error) {
    data, _ := json.Marshal(p)
    return string(data), nil
}

// Несколько параметров
func ParamsListMarshal(params []Param) ([]string, error) {
    result := make([]string, len(params))
    for i, p := range params {
        data, _ := json.Marshal(p)
        result[i] = string(data)
    }
    return result, nil
}

Выходные параметры

type ProcFieldsMyProc struct {
    Input  string `ar:"input"`
    Result string `ar:"output;serializer:Json"`
}

type SerializersMyProc struct {
    Result *MyResultStruct `ar:"pkg:github.com/myapp/types;object:ResultS"`
}

Обработка ошибок

stats := stats.New(ctx)
stats.SetUserId("12345")

err := stats.Call(ctx)
if err != nil {
    // Ошибка вызова процедуры
    if errors.Is(err, octopus.ErrProcNotFound) {
        return fmt.Errorf("procedure not found")
    }
    return err
}

Метрики

Метрика Описание
call_proc Время вызова процедуры
call_proc_preparedb Ошибка подготовки соединения
call_proc (error) Ошибка выполнения процедуры

Best Practices

1. Валидируйте входные параметры

func CallStats(ctx context.Context, userId string) (*Stats, error) {
    if userId == "" {
        return nil, fmt.Errorf("userId is required")
    }

    stats := stats.New(ctx)
    stats.SetUserId(userId)
    // ...
}

2. Используйте таймауты

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

stats.Call(ctx)

3. Логируйте вызовы

log.Printf("Calling calculate_stats for user %s", userId)
if err := stats.Call(ctx); err != nil {
    log.Printf("calculate_stats failed: %v", err)
    return err
}

Следующие шаги