Актерский веб-сервис - как это сделать правильно? - PullRequest
22 голосов
/ 10 февраля 2012

За последние несколько месяцев я и мои коллеги успешно создали серверную систему для отправки push-уведомлений на устройства iPhone. Как правило, пользователь регистрируется для этих уведомлений через веб-сервис RESTful ( Spray-Server , недавно обновленный для использования Spray-can в качестве уровня HTTP), и логика планирует одно или несколько сообщений для отправки в будущем, используя планировщик Акки.

Эта система, как мы ее создали, просто работает: она может обрабатывать сотни, может быть, даже тысячи HTTP-запросов в секунду, и может отправлять уведомления со скоростью 23 000 в секунду - возможно, даже больше, если мы уменьшим вывод журнала, добавьте несколько действующих лиц отправителя уведомлений (и, следовательно, больше подключений к Apple), и в используемой нами библиотеке Java может потребоваться некоторая оптимизация ( java-apns ).

Этот вопрос о том, как сделать это правильно (тм). Мой коллега, гораздо более осведомленный о 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 в «бизнес-логику», в данном случае постоянную. Персистер теперь получает дополнительную ответственность, то есть звонит request.complete, и знает о том, в какой системе он работает, то есть о том, что он является частью веб-службы.

Каков был бы правильный способ справиться с такой ситуацией, чтобы постоянный субъект не знал, что он является частью службы http, и ему не нужно знать, как вывести сгенерированный идентификатор?

Я думаю, что запрос все еще должен быть передан действующему субъекту, но вместо постоянного субъекта, вызывающего 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 в сообщениях актера, которые могут проходить по всему приложению, нормальным подходом, или есть лучший способ «запомнить», на какой запрос должен быть отправлен ответ после передачи запрос через приложение?

Ответы [ 2 ]

3 голосов
/ 10 февраля 2012

Что касается вашего первого вопроса, да, вы на правильном пути. (Хотя я также хотел бы увидеть несколько альтернативных способов решения этой проблемы).

Одно из предложений, которое у меня есть, - оградить актера 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))
}

Что касается вашего второго вопроса, классы актеров на самом деле ничем не отличаются от обычных классов. Но вам нужно убедиться, что вы вызываете метод receive суперкласса, чтобы он мог обрабатывать свои собственные сообщения. У меня были некоторые другие способы сделать это в моем первоначальном ответе , но я думаю, что я предпочитаю связывание частичных функций как это :

class SpecialHttpService extends HttpService {
  override def receive = super.receive orElse {
    case SpecialMessage(x) =>
      // handle special message
  }
}
0 голосов
/ 14 марта 2012

Вы также можете использовать директиву о продукции.Это позволяет вам отделить фактическую сортировку от завершения запроса:

get {
  produce(instanceOf[Person]) { personCompleter =>
    databaseActor ! ShowPersonJob(personCompleter)
  }
}

Директива продукта в этом примере извлекает функцию Person => Unit, которую вы можете использовать для прозрачного завершения запроса глубоко внутри уровня бизнес-логики., который не должен знать о спрей.

https://github.com/spray/spray/wiki/Marshalling-Unmarshalling

...