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

Як скоротити URL у Ruby за допомогою Net::HTTP і Faraday

Скоротіть URL у Ruby за допомогою Net::HTTP без гемів, а потім додайте тайм-аути, повторні спроби та ідемпотентність або доручіть усю роботу Faraday у застосунку Rails.

Marius Voß
DevRel · edge infra
Як скоротити URL у Ruby: POST-запит Net::HTTP надсилає URL призначення до API скорочувача, а з відповіді JSON зчитується коротке посилання

Скорочення URL у Ruby - це один POST-запит. Надішліть довге посилання до API скорочувача за допомогою Net::HTTP, передайте свій API-ключ як Bearer-токен і розберіть short_url із отриманого JSON. Гем не потрібен, що важливо, коли скрипт має працювати на машині, яку ви не контролюєте.

Формат кінцевої точки та модель автентифікації нижче описано в огляді безкоштовного API скорочувача URL; цей матеріал є версією для Ruby загальної інструкції зі скорочення URL, де розглянуто шлях через панель керування.

Та сама серія для інших середовищ виконання: Python, JavaScript, PHP та Go. Назви полів належать Elido, але така структура підходить більшості сучасних скорочувачів.

Найшвидший спосіб: Net::HTTP.post

Два require, один виклик:

require "json"
require "net/http"

response = Net::HTTP.post(
  URI("https://api.elido.app/v1/links"),
  { destination_url: "https://example.com/spring-sale?utm_source=newsletter" }.to_json,
  "Authorization" => "Bearer #{ENV.fetch('ELIDO_API_KEY')}",
  "Content-Type" => "application/json",
)

raise "shorten failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

puts JSON.parse(response.body)["short_url"] # => https://s.elido.me/ab12cd

ENV.fetch, а не ENV[], використано навмисно: відсутній ключ одразу спричиняє помилку, замість того щоб надіслати Bearer і повернутися з незрозумілою відповіддю 401.

Рядок unless response.is_a?(Net::HTTPSuccess) часто пропускають. Net::HTTP створює виняток у разі непрацюючого сокета або тайм-ауту, але більше ні в яких випадках, тому відповідь 401 із тілом помилки надходить так, ніби це успіх, доки JSON.parse не поверне вам хеш без short_url.

Додайте тайм-аути, повторні спроби та ключ ідемпотентності

У Net::HTTP.post є вада, яку не можна виправити: він не приймає аргументів тайм-ауту. Для всього, що працює без нагляду, перейдіть на Net::HTTP.start, де вони доступні.

Поки ви це робите, обробіть помилку, яку наївна повторна спроба лише погіршує. Якщо POST досягає API, а відповідь губиться на зворотному шляху, код завершується через тайм-аут, повторює запит і створює друге посилання для того самого призначення. Стабільний Idempotency-Key усуває цю проблему: захешуйте призначення, і API поверне початкове посилання замість створення нового.

require "digest"

def shorten(destination, attempts: 3)
  uri = URI("https://api.elido.app/v1/links")
  key = Digest::SHA256.hexdigest(destination) # stable across retries and re-runs

  attempts.times do |attempt|
    response = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
                               open_timeout: 5, read_timeout: 10) do |http|
      request = Net::HTTP::Post.new(uri)
      request["Authorization"]   = "Bearer #{ENV.fetch('ELIDO_API_KEY')}"
      request["Content-Type"]    = "application/json"
      request["Idempotency-Key"] = key
      request.body = { destination_url: destination }.to_json
      http.request(request)
    end

    case response
    when Net::HTTPSuccess         then return JSON.parse(response.body)["short_url"]
    when Net::HTTPTooManyRequests then sleep(response["Retry-After"].to_i.clamp(1, 60))
    when Net::HTTPServerError     then sleep(2**attempt) # 1s, 2s, 4s
    else raise "shorten failed: #{response.code} #{response.body}"
    end
  end

  raise "shorten failed after #{attempts} attempts"
end

case за класом відповіді читається краще, ніж купа порівнянь кодів стану, і чітко показує політику: для 429 потрібно чекати стільки, скільки просить сервер, для 5xx застосовується експоненційна затримка, а все інше, зокрема 401 або 422, створює виняток із першої спроби, бо повторна спроба це не виправить. У детальному матеріалі про ліміти швидкості та ідемпотентність повністю описано семантику заголовків.

POST-запит Ruby Net::HTTP надсилає URL призначення та Idempotency-Key до кінцевої точки посилань скорочувача з Bearer-токеном, API повертає HTTP 201 із short_url, а цикл повторних спроб очікує у разі відповідей 429 і 5xx

Хочете запустити це як є? Створіть ключ на безкоштовному плані, експортуйте його як ELIDO_API_KEY, і кожен фрагмент коду на цій сторінці працюватиме без змін.

Версія Faraday для вже наявного застосунку

У застосунку Rails саморобний цикл має неправильну структуру. Faraday надає один об'єкт з'єднання з підключеним заголовком автентифікації, JSON в обох напрямках і політику повторних спроб, яку оголошують, а не пишуть вручну:

# Gemfile: gem "faraday"  and  gem "faraday-retry"

ELIDO = Faraday.new(url: "https://api.elido.app") do |f|
  f.request :json
  f.request :retry, max: 3, interval: 0.5, backoff_factor: 2,
                    retry_statuses: [429, 500, 502, 503, 504]
  f.response :json
  f.response :raise_error
  f.headers["Authorization"] = "Bearer #{ENV.fetch('ELIDO_API_KEY')}"
  f.options.timeout = 10
end

def shorten(destination)
  ELIDO.post("/v1/links",
             { destination_url: destination },
             { "Idempotency-Key" => Digest::SHA256.hexdigest(destination) })
       .body["short_url"]
end

Перед копіюванням зверніть увагу на дві речі. У Faraday 2 проміжне програмне забезпечення для повторних спроб перенесено до окремого гема faraday-retry, тому в Gemfile потрібен окремий рядок. А raise_error змінює поведінку Net::HTTP на протилежну: тепер 4xx і 5xx створюють Faraday::ClientError та Faraday::ServerError, що й потрібно для завдання, яке має повторювати спробу, а не зберігати nil.

Порівняння Ruby Net::HTTP і Faraday для виклику API скорочувача URL: скрипт без залежностей проти об'єкта з'єднання з проміжним програмним забезпеченням для повторних спроб у застосунку Rails

Скорочення списку без перевищення ліміту швидкості

Ruby звільняє глобальне блокування, поки потік очікує на ввід-вивід, тому тут потоки справді корисні навіть у CRuby. Не можна лише створювати по одному потоку на URL: CSV із 2 000 рядків перетвориться на 2 000 сокетів і стіну відповідей 429. Queue у поєднанні з фіксованим пулом утримує рівень паралельності сталим незалежно від довжини списку.

def shorten_all(urls, concurrency: 8)
  queue   = Queue.new
  results = {}
  mutex   = Mutex.new

  urls.each { |u| queue << u }
  concurrency.times { queue << :done }

  workers = concurrency.times.map do
    Thread.new do
      while (url = queue.pop) != :done
        value = begin
          shorten(url)
        rescue => e
          "ERROR: #{e.message}" # one bad row must not sink the batch
        end
        mutex.synchronize { results[url] = value }
      end
    end
  end

  workers.each(&:join)
  results
end

Ключі результатів у вигляді початкових URL роблять часткову помилку видимою, а пакет - придатним до повторного запуску. Mutex не є необов'язковим: звичайний Hash, до якого одночасно записують вісім потоків, створює стан перегонів, і це дасть про себе знати саме під час важливого запуску.

У Rails обгорніть shorten в Active Job і доручіть адаптеру черги керувати затримкою. Повторна спроба всередині дії контролера утримує потік Puma під час кожного sleep, а імпорт, обмежений за швидкістю, може непомітно виснажити веб-процес. Якщо ви підключаєте це до процесу публікації, у матеріалі про вебхуки для подій посилань описано, як отримувати дані про кліки без опитування.

Що обрати

Завдання rake або одноразовий скрипт: Net::HTTP.post, чотири рядки - і готово. Заплановане виконання: версія Net::HTTP.start із тайм-аутами та ключем ідемпотентності. Застосунок Rails, у якому вже є Faraday: об'єкт з'єднання з повторними спробами, оголошеними один раз і повторно використаними всюди.

Вибір майже не має значення порівняно з трьома базовими звичками. Беріть ключ із середовища, встановлюйте явний тайм-аут і перевіряйте клас відповіді до розбору тіла. На сторінці API та SDK наведено список згенерованих клієнтів, якщо ви не хочете підтримувати все це самостійно, а рішення для розробників охоплюють інші можливості API після того, як посилання почнуть створюватися з коду.

Читайте основну серію

Цей матеріал належить до інженерного кластера. Почніть із посібника з безкоштовного API скорочувача URL, щоб розібратися зі структурою кінцевої точки й автентифікацією, а потім прочитайте матеріал про ліміти швидкості та ідемпотентність, щоб коректно працювати під навантаженням. Актуальний довідник - це документація API, а в матеріалі скорочувачі URL для розробників описано, що перевірити в API, перш ніж будувати на його основі.

У блозі також

Поширені запитання

Як скоротити URL у Ruby?

Надішліть довгий URL до API скорочувача методом POST через Net::HTTP, передавши API-ключ у заголовку Bearer, а URL призначення - у тілі JSON, після чого прочитайте short_url з розібраної відповіді. Net::HTTP.post - це однорядковий виклик стандартної бібліотеки, тому для першої версії нічого не потрібно встановлювати.

Чи потрібен мені гем, щоб скорочувати URL у Ruby?

Ні. Net::HTTP і бібліотека json постачаються з Ruby та охоплюють увесь виклик. Faraday виправдовує використання, коли вам потрібні повторно використовуване з'єднання, проміжне програмне забезпечення для повторних спроб і автоматичне кодування JSON, що зазвичай актуально в застосунку Rails, а не в окремому скрипті.

Як установити тайм-аут для Net::HTTP?

Використовуйте Net::HTTP.start з open_timeout і read_timeout замість скороченого Net::HTTP.post, де немає способу встановити жоден із них. Без цих параметрів зависле з'єднання може утримувати виконавця всі 60 секунд за замовчуванням, а у фоновому завданні це означає втрату одного доступного слота.

Чому Net::HTTP не створює виняток для 401?

Тому що 401 - це дійсна відповідь HTTP, а не помилка передавання. Net::HTTP створює винятки лише для помилок сокета й тайм-аутів, тому клас відповіді потрібно перевірити самостійно за допомогою is_a?(Net::HTTPSuccess) або прочитати response.code. Проміжне програмне забезпечення Faraday raise_error робить це за вас.

Як скоротити багато URL одночасно в Ruby?

Помістіть URL у Queue і запустіть невеликий пул потоків, зазвичай приблизно з восьми. Ruby звільняє глобальне блокування під час вводу-виводу, тому потоки справді перекривають очікування мережі, а фіксований пул утримує кількість запитів нижче ліміту швидкості API, який необмежений цикл із потоком на кожен URL одразу перевищив би.

Де має бути цикл повторних спроб у застосунку Rails?

У фоновому завданні, а не в дії контролера. Повторна спроба, яка присипляє потік на кілька секунд, увесь цей час утримує потік Puma, тому пакет, обмежений за швидкістю, може виснажити веб-процес. Active Job із адаптером черги обробляє затримку й без додаткової роботи надає історію повторних спроб.

Спробуйте Elido

Вставте URL - отримайте коротке посилання

Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.

Безкоштовно, без реєстрації · 2 на день

Спробуйте Elido

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

Теги
how to shorten a url in ruby
ruby url shortener
shorten url ruby net http
url shortener api ruby
faraday post json
bulk shorten urls ruby

Читати далі