Актерский вебсервис - Как это сделать правильно?
В последние несколько месяцев я и мои коллеги успешно создали серверную систему для отправки push-уведомлений на устройства iPhone. В основном пользователь регистрируется для этих уведомлений через веб-сервис RESTful (Spray-Server, недавно обновленный для использования Spray-can в качестве уровня HTTP), а логика планирует одно или несколько сообщений для отправки в будущем с использованием планировщика Akka.
Эта система, как мы ее построили, просто работает: она может обрабатывать сотни, а может быть, и тысячи HTTP-запросов в секунду, и может отправлять уведомления со скоростью 23 000 в секунду - возможно, даже больше, если мы уменьшаем выход журнала, добавьте несколько агентов отправителей уведомлений (и, следовательно, больше связей с Apple), и может быть какая-то оптимизация в используемой библиотеке Java (java-apns).
Этот вопрос касается того, как это сделать Right (tm). Мой коллега, гораздо более осведомленный о Scala и актерских системах в целом, отметил, что приложение не является "чистой" актерской системой - и он прав. Теперь мне интересно, как это сделать правильно.
В настоящий момент у нас есть единственный актер Spray HttpService
, а не подклассы, который инициализируется набором директив, который описывает нашу HTTP-сервисную логику. В настоящее время мы очень упрощены, у нас есть такие директивы:
post {
content(as[SomeBusinessObject]) { businessObject => request =>
// store the business object in a MongoDB back-end and wait for the ID to be
// returned; we want to send this back to the user.
val businessObjectId = persister !! new PersistSchedule(businessObject)
request.complete("/businessObject/%s".format(businessObjectId))
}
}
Теперь, если я получу это правильно, "ожидание ответа" от актера - нет-нет в актерском программировании (плюс!!) устарел). Я верю, что это "правильный" способ сделать это: передать объект request
объекту persister
в сообщении и вызвать его вызов request.complete
, как только он получит сгенерированный идентификатор из back- конец.
Я переписал один из маршрутов в моем приложении, чтобы сделать именно это; в сообщении, которое отправляется актеру, объект запроса/ссылка также отправляется. Кажется, что это работает так, как будто:
content(as[SomeBusinessObject]) { businessObject => request =>
persister ! new PersistSchedule(request, businessObject)
}
Моя главная проблема здесь состоит в том, что мы, кажется, передаем объект request
в "бизнес-логику", в этом случае persister. Теперь persister получает дополнительную ответственность, т.е. Вызывает request.complete
и знание о том, в какой системе он работает, т.е. Что он является частью веб-службы.
Каким будет правильный способ справиться с подобной ситуацией, чтобы актер-персик не осознавал, что он является частью службы http, и не нужно знать, как вывести сгенерированный идентификатор?
Я думаю, что запрос все равно должен быть передан актеру persister, но вместо актера persister, вызывающего request.complete, он отправляет сообщение обратно актеру HttpService (сообщение SchedulePersisted(request, businessObjectId)
), которое просто вызывает request.complete("/businessObject/%s".format(businessObjectId))
. В основном:
def receive = {
case SchedulePersisted(request, businessObjectId) =>
request.complete("/businessObject/%s".format(businessObjectId))
}
val directives = post {
content(as[SomeBusinessObject]) { businessObject => request =>
persister ! new PersistSchedule(request, businessObject)
}
}
Я нахожусь на правильном пути с этим подходом?
Меньший вторичный вопрос spray-server
, хорошо ли подклассом HttpService
и переопределить метод получения, или я буду нарушать такие вещи? (Я не имею понятия о субъектах подкласса или о том, как передавать нераспознанные сообщения "родительскому" актеру)
Заключительный вопрос, передает объект/ссылку request
в сообщениях-актерах, которые могут проходить по всему приложению в порядке, или есть лучший способ "запомнить", какой запрос должен быть отправлен ответ после прохождения запросить через приложение?
Ответы
Ответ 1
Что касается вашего первого вопроса, да, вы на правильном пути. (Хотя я также хотел бы увидеть некоторые альтернативные способы решения этой проблемы).
Одно из моих предложений - изолировать актера persister
от знания о запросах вообще. Вы можете передать запрос как тип Any
. Ваш помощник в вашем сервисном коде может автоматически перевести файл cookie в Request
.
case class SchedulePersisted(businessObjectId: String, cookie: Any)
// in your actor
override def receive = super.receive orElse {
case SchedulePersisted(businessObjectId, request: Request) =>
request.complete("/businessObject/%s".format(businessObjectId))
}
Что касается вашего второго вопроса, классы актеров действительно не отличаются от обычных классов. Но вам нужно убедиться, что вы вызываете метод superclass receive
, чтобы он мог обрабатывать свои собственные сообщения. У меня были другие способы сделать это в моем первоначальном ответе, но я думаю, что предпочитаю цепочки частичных выполняет следующие функции:
class SpecialHttpService extends HttpService {
override def receive = super.receive orElse {
case SpecialMessage(x) =>
// handle special message
}
}
Ответ 2
Вы также можете использовать директиву продукта. Это позволяет отделить фактическую сортировку от завершения запроса:
get {
produce(instanceOf[Person]) { personCompleter =>
databaseActor ! ShowPersonJob(personCompleter)
}
}
Директива product в этом примере извлекает функцию Person = > Unit, которую вы можете использовать, чтобы выполнить запрос прозрачно глубоко внутри уровня бизнес-логики, который не должен знать о спрее.
https://github.com/spray/spray/wiki/Marshalling-Unmarshalling