Эффективный способ рендеринга тонны JSON на Heroku

#ruby #json #heroku #sinatra #mongoid

#ruby #json #heroku #синатра #mongoid

Вопрос:

Я создал простой API с одной конечной точкой. Он очищает файлы и в настоящее время содержит около 30 000 записей. В идеале я хотел бы иметь возможность извлекать все эти записи в формате JSON одним HTTP-вызовом.

Вот мой код представления Sinatra:

 require 'sinatra'
require 'json'
require 'mongoid'

Mongoid.identity_map_enabled = false

get '/' do
  content_type :json
  Book.all
end
  

Я пробовал следующее:
использование multi_json с

 require './require.rb'
require 'sinatra'
require 'multi_json'
MultiJson.engine = :yajl

Mongoid.identity_map_enabled = false

get '/' do
  content_type :json
  MultiJson.encode(Book.all)
end
  

Проблема с этим подходом заключается в том, что я получаю ошибку R14 (превышена квота памяти). Я получаю ту же ошибку, когда пытаюсь использовать драгоценный камень ‘oj’.

Я бы просто объединил все в одну длинную строку Redis, но услуга redis от Heroku стоит 30 долларов в месяц за необходимый мне размер экземпляра (> 10 МБ).

Мое текущее решение состоит в том, чтобы использовать фоновую задачу, которая создает объекты и заполняет их jsonified объектами с предельным размером объекта Mongoid (16 МБ). Проблемы с этим подходом: Рендеринг по-прежнему занимает почти 30 секунд, и мне приходится запускать постобработку в принимающем приложении, чтобы правильно извлечь json из объектов.

Есть ли у кого-нибудь идея получше, как я могу рендерить json для 30 тыс. записей за один вызов, не переключаясь с Heroku?

Комментарии:

1. даже 100 объектов слишком много для одного вызова API. если вы действительно хотите получить это в одном вызове API, вам следует разбить запрос / ответ на страницы. затем на стороне клиента используйте каждую страницу в виде отдельных вызовов по мере перебора полного набора записей.

Ответ №1:

Похоже, вы хотите передавать JSON непосредственно клиенту, а не создавать все это в памяти. Вероятно, это лучший способ сократить использование памяти. Вы могли бы, например, использовать yajl для кодирования JSON непосредственно в поток.

Редактировать: я переписал весь код для yajl , потому что его API намного более привлекательный и позволяет создавать гораздо более чистый код. Я также включил пример для чтения ответа по частям. Вот помощник по потоковому массиву JSON, который я написал:

 require 'yajl'

module JsonArray
  class StreamWriter
    def initialize(out)
      super()
      @out = out
      @encoder = Yajl::Encoder.new
      @first = true
    end

    def <<(object)
      @out << ',' unless @first
      @out << @encoder.encode(object)
      @out << "n"
      @first = false
    end
  end

  def self.write_stream(app, amp;block)
    app.stream do |out|
      out << '['
      block.call StreamWriter.new(out)
      out << ']'
    end
  end
end
  

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

 require 'sinatra'
require 'mongoid'

Mongoid.identity_map_enabled = false

# use a server that supports streaming
set :server, :thin

get '/' do
  content_type :json
  JsonArray.write_stream(self) do |json|
    Book.all.each do |book|
      json << book.attributes
    end
  end
end
  

Для декодирования на стороне клиента вы можете читать и анализировать ответ по частям, например, с em-http помощью . Обратите внимание, что для этого решения требуется, чтобы память клиентов была достаточно большой для хранения всего массива объектов. Вот соответствующий помощник потокового синтаксического анализатора:

 require 'yajl'

module JsonArray
  class StreamParser
    def initialize(amp;callback)
      @parser = Yajl::Parser.new
      @parser.on_parse_complete = callback
    end

    def <<(str)
      @parser << str
    end
  end

  def self.parse_stream(amp;callback)
    StreamParser.new(amp;callback)
  end
end
  

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

 require 'em-http'

parser = JsonArray.parse_stream do |object|
  # block is called when we are done parsing the
  # entire array; now we can handle the data
  p object
end

EventMachine.run do
  http = EventMachine::HttpRequest.new('http://localhost:4567').get
  http.stream do |chunk|
    parser << chunk
  end
  http.callback do
    EventMachine.stop
  end
end
  

Альтернативное решение

На самом деле вы могли бы значительно упростить все это, если бы отказались от необходимости генерировать «правильный» массив JSON. Приведенное выше решение генерирует JSON в такой форме:

 [{ ... book_1 ... }
,{ ... book_2 ... }
,{ ... book_3 ... }
...
,{ ... book_n ... }
]
  

Однако мы могли бы транслировать каждую книгу как отдельный JSON и, таким образом, уменьшить формат до следующего:

 { ... book_1 ... }
{ ... book_2 ... }
{ ... book_3 ... }
...
{ ... book_n ... }
  

Тогда код на сервере был бы намного проще:

 require 'sinatra'
require 'mongoid'
require 'yajl'

Mongoid.identity_map_enabled = false
set :server, :thin

get '/' do
  content_type :json
  encoder = Yajl::Encoder.new
  stream do |out|
    Book.all.each do |book|
      out << encoder.encode(book.attributes) << "n"
    end
  end
end
  

А также клиент:

 require 'em-http'
require 'yajl'

parser = Yajl::Parser.new
parser.on_parse_complete = Proc.new do |book|
  # this will now be called separately for every book
  p book
end

EventMachine.run do
  http = EventMachine::HttpRequest.new('http://localhost:4567').get
  http.stream do |chunk|
    parser << chunk
  end
  http.callback do
    EventMachine.stop
  end
end
  

Самое замечательное, что теперь клиенту не нужно ждать полного ответа, а вместо этого он анализирует каждую книгу отдельно. Однако это не сработает, если один из ваших клиентов ожидает один большой массив JSON.

Комментарии:

1. @MichaelQLarson пожалуйста, посмотрите мою правку, в ответе теперь есть реальный рабочий код 😉

2. Спасибо! Я новичок в OJ и не могу заставить ваш новый код работать на клиенте (который использует конечную точку Sinatra). Он выдает следующую ошибку: Oj::ParseError: Hash/Object not terminated at line 1, column 30 . На основе документации , которую я пытаюсь запустить oj_json = open("MySinatraApp.herokuapp.com"); records = Oj.load(oj_json) . Вы знаете, что я могу делать неправильно?

3. Эта комбинация open-uri и oj , похоже, не способна обрабатывать потоковую передачу. Я переписал код с использованием yajl и включил пример использования клиента net/http . Приветствия