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

Продвинутая работа с очередями

В этом примере показано, как собирать сложные сценарии на базе Queue:
  • разбивать процесс на этапы и добавлять задачи динамически;
  • реагировать на выбор пользователя и результат задач;
  • обрабатывать ошибки и повторные попытки без выхода из TUI.
Все примеры используют публичный API пакета ziva. Их можно поместить в отдельный пакет examples и запускать по мере необходимости.

Шаблон многоэтапного сценария

// DeployCluster запускает последовательный сценарий, который спрашивает настройки
// у оператора, отображает прогресс и сохраняет результаты для последующей логики.
func DeployCluster() error {
    summary := ziva.NewFuncTask(
        "Инициализация окружения",
        runPreflightChecks,
        ziva.WithSummaryFunction(func() []string {
            return []string{"DNS: ok", "Сеть: ok"}
        }),
        ziva.WithStopOnError(true),
    )

    topology := ziva.NewSingleSelectTask(
        "Топология",
        []string{
            "single-node::Один узел",
            "multi-node::Кластер из нескольких узлов",
            "ha::Высокая доступность",
        },
    ).WithDefaultItem("multi-node")

    nodes := ziva.NewInputTask(
        "Количество узлов",
        "Сколько рабочих узлов разворачиваем?",
    ).
        WithInputType(ziva.InputTypeNumber).
        WithValidator(ziva.DefaultValidators.Range(1, 20)).
        WithTimeout(30*time.Second, "3")

    queue := ziva.NewQueue("Развертывание Ziva").
        WithAppName("Ziva CLI").
        WithTasksNumbered(true, "[%02d]")

    queue.AddTasks(summary, topology, nodes)
    if err := queue.Run(); err != nil {
        return err
    }

    // В зависимости от выбранной топологии добавим дополнительные задачи.
    followUp := ziva.NewQueue("Дополнительные шаги")

    switch topology.GetSelected() {
    case "single-node":
        followUp.AddTasks(planBackupTask(), confirmMetricsTask())
    case "multi-node":
        followUp.AddTasks(configureIngressTask(), scaleOutTask(nodes))
    case "ha":
        followUp.AddTasks(haPrecheckTask(), configureQuorumTask(nodes))
    }

    return followUp.Run()
}

Ключевые идеи

  • Два запуска очереди. Первая очередь собирает ввод и сохраняет задачи для повторного использования. Вторая очередь формируется динамически.
  • Summary-функция. FuncTask с WithSummaryFunction позволяет отобразить итог прямо под задачей и лучше информировать пользователя.
  • Таймауты. WithTimeout гарантирует, что сценарий не зависнет в headless-среде.

Повторные попытки с подтверждением пользователя

func RunWithRetries(label string, fn func() error, maxAttempts int) error {
    attempt := 1

    for {
        task := ziva.NewFuncTask(
            fmt.Sprintf("%s · попытка %d", label, attempt),
            fn,
            ziva.WithStopOnError(false),
        )

        queue := ziva.NewQueue(label)
        queue.AddTasks(task)

        if err := queue.Run(); err == nil {
            return nil
        }

        if attempt >= maxAttempts {
            return fmt.Errorf("достигнут лимит попыток (%d)", maxAttempts)
        }

        retry := ziva.NewYesNoTask(
            "Повторить попытку",
            fmt.Sprintf("Попытка %d завершилась ошибкой. Повторить?", attempt),
        ).WithDefaultYesAndTimeout(10 * time.Second)

        confirm := ziva.NewQueue("Решение о повторе")
        confirm.AddTasks(retry)
        confirm.Run()

        if !retry.IsYes() {
            return fmt.Errorf("операция отменена пользователем на попытке %d", attempt)
        }

        attempt++
    }
}

Когда пригодится

  • интеграция с нестабильными сервисами;
  • обновления прошивок на устройствах с малонадежной связью;
  • выполнение длительных действий, которые стоит перезапустить по согласию пользователя.

Построение задач на лету

func ConfigureServers() error {
    count := ziva.NewInputTask("Количество серверов", "Сколько серверов конфигурируем?").
        WithInputType(ziva.InputTypeNumber).
        WithValidator(ziva.DefaultValidators.Range(1, 8))

    baseQueue := ziva.NewQueue("Параметры конфигурации")
    baseQueue.AddTasks(count)
    if err := baseQueue.Run(); err != nil {
        return err
    }

    n, _ := strconv.Atoi(count.GetValue())

    var tasks []ziva.Task
    for i := 1; i <= n; i++ {
        prefix := fmt.Sprintf("Сервер %d", i)

        host := ziva.NewInputTask(prefix+" · хост", "Введите FQDN")
        host.WithValidator(ziva.DefaultValidators.Domain())

        port := ziva.NewInputTask(prefix+" · порт", "Введите порт")
        port.WithInputType(ziva.InputTypeNumber).
            WithValidator(ziva.DefaultValidators.Port())

        tasks = append(tasks, host, port)
    }

    servers := ziva.NewQueue("Настройка серверов").
        WithTasksNumbered(true, "[%02d]")
    servers.AddTasks(tasks...)
    return servers.Run()
}
При генерации большого количества задач учитывайте возможности устройства. Если сценарий может создавать сотни шагов, разбивайте их на отдельные очереди и запускайте блоками.

Мини-чеклист для сложных сценариев

  • Сохраняйте ссылки на задачи, чьи результаты понадобятся позже.
  • Используйте отдельные очереди для разных этапов — это упрощает логику и вывод.
  • Вызывайте AutoConfigure() при старте CLI, чтобы Ziva подобрал безопасные настройки.
  • Для встроенных устройств комбинируйте EnableEmbeddedMode() и WithOutResultLine() — так вывод остаётся компактным.
  • Помните, что очередь последовательно выполняет задачи; если нужна «параллельность», запускайте отдельные очереди из FuncTask.
Следующий пример показывает, как использовать валидаторы для надёжных форм ввода.
I