Elido
6 хв читанняІнженерія

Запуск міграції з Rebrandly: пагінація по 25 записів на сторінку та 30-хвилинний бюджет

Як ми створили імпорт з Rebrandly в один клік для Elido — повільний розмір сторінки, UX фільтрації робочих просторів та те, що ми свідомо не мігруємо.

Marius Voß
DevRel · edge infra
Діаграма конвеєра: REST API Rebrandly зліва, дані якого проходять через воркер імпорту Elido в таблицю посилань, з бічною панеллю, де вказані чисельні гарантії воркера (обмеження 50 тис., 30-хвилинний бюджет, 25 записів на сторінку, токен лише в пам'яті)

Друге джерело міграції в нашому розгортанні Tier-3 було випущено сьогодні. Вставте API-ключ Rebrandly, за бажанням відфільтруйте за робочим простором і натисніть «Start». Через шість-десять хвилин кожен слештег опиняється на вашому домені Elido зі збереженим слагом, де не виникло конфліктів. Міграція з Bitly, яка з'явилася два тижні тому, заклала каркас; Rebrandly — другий вендор, що скористався ним.

Цей пост — інженерний звіт про те, що є специфічним для Rebrandly, що ми залишили ідентичним воркеру Bitly і де API Rebrandly змусило обрати іншу структуру.

Що спільного з Bitly#

Ця функція з самого початку планувалася як одна таблиця та один контракт воркера. Обидва витримали перевірку.

CREATE TABLE import_jobs (
    id                  BIGSERIAL    PRIMARY KEY,
    workspace_id        BIGINT       NOT NULL,
    source_vendor       TEXT         NOT NULL,
    target_domain_id    BIGINT       NOT NULL,
    status              TEXT         NOT NULL DEFAULT 'queued',
    conflict_strategy   TEXT         NOT NULL DEFAULT 'suffix',
    source_filter       JSONB        NOT NULL DEFAULT '{}'::jsonb,
    -- counters + error_log + timestamps elided
);

source_vendor змінюється на rebrandly. source_filter містить {workspace_id: "..."}, коли користувач застосовує фільтр, або {}, коли потрібно імпортувати всі посилання, доступні за цим ключем. Все інше — 30-хвилинний бюджет, обмеження в 50 тис. посилань, стратегія вирішення конфліктів (суфікс/пропуск/помилка), тег imported:rebrandly — ідентичне до шляху Bitly.

Лаунчер на дашборді (apps/web/src/app/dashboard/integrations/[id]/rebrandly-migration-launcher.tsx) структурно є копією лаунчера для Bitly, але без випадаючого списку груп — у Rebrandly є робочі простори (workspaces), а не групи, і ми виставляємо їх як опціональний текстовий фільтр, а не як випадаючий список, оскільки ендпоінт Workspaces використовує пагінацію без автентифікації, а типовий користувач має щонайбільше два робочих простори.

У чому різниця API Rebrandly#

Три моменти:

Розмір сторінки. Rebrandly обмежує одну сторінку 25 посиланнями. Bitly — 100. Тому акаунт на 5000 посилань, який обробляється за 4–8 хвилин у Bitly, забере 6–10 хвилин у Rebrandly. Вузьким місцем є вендор, а не воркер.

Пагінація. Rebrandly використовує параметр запиту last, який приймає ID останнього елемента на попередній сторінці. Bitly повертає URL pagination.next. Обидва варіанти мають курсорний тип; варіант Rebrandly просто здійснює більше запитів. Весь цикл складається з шести рядків:

last := ""
for {
    page, err := w.fetchPage(ctx, opts.Token, opts.WorkspaceID, last)
    if err != nil { /* mark failed */ return }
    if len(page) == 0 { break }
    for _, link := range page {
        // ... resolve slug, insert, update counters ...
    }
    last = page[len(page)-1].ID
}

Ми довіряємо курсору. Якби Rebrandly двічі повернув однаковий last, ми б зациклилися; 30-хвилинний бюджет обмежує шкоду.

Обмеження робочим простором. API-ключ Rebrandly бачить кожне посилання в кожному робочому просторі, до якого належить користувач. Якщо у вас агенційський акаунт із п'ятьма клієнтськими робочими просторами, ви майже напевно захочете імпортувати їх по одному. Лаунчер виставляє це як опціональне текстове поле — вставте ID робочого простору з адресного рядка Rebrandly або залиште порожнім, щоб імпортувати «все, що бачить ключ».

Що ми не мігруємо#

Історію кліків. Дані Rebrandly про кліки доступні лише для тарифів Premium і відображаються як сумарні лічильники на посилання, а не як події кліків. Ми акцентуємо на цьому обмеженні на кожній сторінці, яку бачить користувач — на сторінці рецепта на дашборді, на лендингу /migrate-from/rebrandly, в UI прогресу імпорту та в розділі FAQ. Нові кліки потрапляють в аналітику Elido з моменту перемикання.

UTM-шаблони Rebrandly. Це функція відображення в Rebrandly, яка не має чіткого API для експорту. Перестворіть їх як правила кампаній в Elido — тег imported:rebrandly є інструментом для масового перепризначення.

Стилізація QR. Стандартний QR-код Elido генерується для кожного імпортованого посилання; кастомний дизайн потрібно застосовувати повторно. Більшість користувачів використовують масовий фільтр за тегами, щоб застосувати накладання CTA або кампанії Elido постфактум.

Обробка токенів#

Ідентична до Bitly. Токен ніколи не записується на диск:

bgCtx := context.WithoutCancel(r.Context())
go h.rebrandly.Run(bgCtx, job.ID, imports.RebrandlyJobOptions{
    Token:       req.Token,
    WorkspaceID: req.WorkspaceID,
})

source_token_id залишається NULL. Таблиця service_tokens з ADR-0036 призначена для інтеграцій Tier-2 із токенами, що вставляються (Mailchimp, Brevo, Klaviyo), де повторне використання виправдовує зберігання. Для одноразових міграцій використання лише в пам'яті є правильним операційним рішенням — користувач один раз вставляє токен, воркер виконує роботу, токен видаляється.

context.WithoutCancel (Go 1.21+) зберігає значення контексту — логер, trace ID, дедлайни — але видаляє сигнал скасування, щоб воркер пережив HTTP-запит, який його ініціював. Це той самий патерн, що й у воркера Bitly, і той самий патерн, який використовуватимуть усі майбутні вендори міграції.

Вирішення конфліктів#

Три стратегії, ідентичні до Bitly. Користувач обирає їх під час запуску завдання:

  • suffix (за замовчуванням): додавати mylink-2, mylink-3… до 50 варіантів. Після 50 ми вважаємо це структурною проблемою і видаємо помилку.
  • skip: залишити існуюче посилання Elido, залогувати джерело, зарахувати як пропущене.
  • fail: перервати все завдання при першому конфлікті. Для суворої семантики 1:1.

Пошук слага — це одне індексоване читання на рядок:

func (w *RebrandlyWorker) resolveSlug(ctx context.Context, domainID int64, desired, strategy string) (string, error) {
    if _, err := w.links.GetByDomainSlug(ctx, domainID, desired); err != nil {
        if errors.Is(err, pgx.ErrNoRows) { return desired, nil }
        return "", fmt.Errorf("slug lookup: %w", err)
    }
    // suffix/skip/fail branching identical to bitly.go
}

Ми платимо додатковим читанням на рядок, але отримуємо детермінований перебір суфіксів і більш зрозуміле повідомлення про помилку. Альтернатива — відловлювання порушення унікальності в pgx і розбір назви обмеження з рядка помилки — гірше рішення.

Що піддається вимірюванню#

Ті самі структуровані zap-логи, що й у Bitly. Робочий простір, цільовий домен, стратегія конфліктів, опціональний фільтр робочого простору. Події життєвого циклу завдання — старт, завершення, очищення завислих завдань — існують попередньо, і дашборд опитує ендпоінт кожні дві секунди.

Ми поки що не графікуємо метрики завдань міграції в продакшені. Когорта Bitly дала нам першу реальну базову лінію трафіку; дані Rebrandly мають бути прямо порівнянними, оскільки воркер механічно ідентичний, а відмінності полягають лише у формі пагінації вендора. Перший кандидат на алерт: кількість очищень завислих завдань > 0 за будь-яку годину — це означає, що воркер «впав», а UI користувача завис у стані running.

Відновлюваність і проблема деплою#

Та сама логіка, що й у Bitly. Воркер працює в процесі; деплой під час імпорту вбиває горутину. Ми приймаємо це для v1, тому що:

  1. Більшість завдань завершуються менш ніж за десять хвилин. Деплої рідкісні в час активного імпорту.
  2. Поле import_jobs.last_progress_at плюс cron-завдання для очищення завислих завдань, що працює кожні 5 хвилин, переводить будь-який рядок running без прогресу за останні 30 хвилин у стан failed із чіткою причиною.
  3. Повторний запуск є ідемпотентним при стратегіях suffix і skip — вже імпортовані посилання виявляються на другому проході та вирішуються згідно зі стратегією.

Для акаунтів, де посилань понад 10 000, відновлюваність виправдовує себе — ми записуємо курсор last від Rebrandly в import_jobs.source_filter і продовжуємо з того місця, де зупинилися. Це наступна ітерація; чотири інших джерела міграції отримають користь від тієї самої зміни, як тільки ми її випустимо.

Що далі#

Той самий каркас, ще три вендори, які будуть інтегровані в ту саму таблицю import_jobs.

  • Short.ioGET /links?limit=150&domain_id=…. Пагінація по доменах; ми попросимо користувача обрати джерельний домен, а не робочий простір.
  • Dub.coGET /api/links?projectSlug=…&limit=100. Збереження папок + тегів; це найчистіша реалізація з усіх чотирьох.
  • TinyURL — Pro/Bulk REST API. Безкоштовний TinyURL не має API і ніколи не мав; цей шлях залишається ручним.

Кожен із них буде працювати через той самий UI опитування на дашборді та патерн тегів imported:<vendor>. Специфічний для вендора воркер залишається в services/api-core/internal/imports/<vendor>.go.

Якщо ви відкладали порівняння з Rebrandly через те, що шлях міграції не був задокументований, тепер він задокументований. Спробуйте — від API-ключа до останнього імпортованого посилання менше ніж за десять хвилин для типових акаунтів.

Пов'язане в блозі#

Спробуйте Elido

URL-скорочувач із хостингом у ЄС: власні домени, глибока аналітика, відкритий API. Безкоштовний тариф — без кредитної картки.

Теги
rebrandly migration
url shortener
go worker
data migration
engineering
tier 3 integrations

Читати далі