#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
. Приветствия