java - lightbend - Diseño correcto en akka.-Entrega de mensajes
akka wikipedia (1)
Voy a tratar de responder algunas de estas preguntas para usted. No voy a tener respuestas concretas para todo, pero espero poder guiarlo en la dirección correcta.
Para empezar, deberá hacer un cambio en la forma en que está comunicando la solicitud a los 3 actores que realizan búsquedas de libros. El uso de un ScatterGatherFirstCompletedRouter
probablemente no sea el enfoque correcto aquí. Este enrutador solo esperará una respuesta de uno de los enrutados (el primero en responder), por lo que su conjunto de resultados estará incompleto ya que no contendrá los resultados de los otros 2 enrutados. También hay un BroadcastRouter
, pero eso no se ajustará a sus necesidades, ya que solo maneja a tell (!)
Y no a ask (?)
. Para hacer lo que quiere hacer, una opción es enviar la solicitud a cada destinatario, obtener Futures
para las respuestas y luego combinarlos en un Future
agregado usando Future.sequence
. Un ejemplo simplificado podría verse así:
case class SearchBooks(title:String)
case class Book(id:Long, title:String)
class BookSearcher extends Actor{
def receive = {
case req:SearchBooks =>
val routees:List[ActorRef] = ...//Lookup routees here
implicit val timeout = Timeout(10 seconds)
implicit val ec = context.system.dispatcher
val futures = routees.map(routee => (routee ? req).mapTo[List[Book]])
val fut = Future.sequence(futures)
val caller = sender //Important to not close over sender
fut onComplete{
case Success(books) => caller ! books.flatten
case Failure(ex) => caller ! Status.Failure(ex)
}
}
}
Ahora ese no será nuestro último código, pero es una aproximación de lo que su muestra estaba tratando de hacer. En este ejemplo, si cualquiera de los enrutadores descendentes falla / se agota el tiempo de espera, vamos a alcanzar nuestro bloqueo de Failure
, y la persona que llama también recibirá una falla. Si todos tienen éxito, la persona que llama obtendrá en su lugar la lista agregada de objetos de Book
.
Ahora en sus preguntas. Primero, pregunta si debe enviar una solicitud a todos los actores nuevamente si no obtiene una respuesta de uno de los destinatarios dentro del tiempo de espera. La respuesta a esta pregunta realmente depende de ti. ¿Le permitiría a su usuario en el otro extremo ver un resultado parcial (es decir, los resultados de 2 de los 3 actores), o siempre tiene que ser el conjunto completo de resultados cada vez? Si la respuesta es sí, podría modificar el código que se está enviando a los destinatarios para que se vea así:
val futures = routees.map(routee => (routee ? req).mapTo[List[Book]].recover{
case ex =>
//probably log something here
List()
})
Con este código, si alguno de los enrutados se agota o falla por algún motivo, una lista vacía de ''Libro'' se sustituirá por la respuesta en lugar del fallo. Ahora, si no puede vivir con resultados parciales, entonces podría reenviar la solicitud completa nuevamente, pero debe recordar que probablemente haya alguien en el otro extremo esperando los resultados de sus libros y que no quieran esperar por siempre.
Para su segunda pregunta, pregunta si ¿qué pasa si su tiempo de espera es prematuro? El valor de tiempo de espera que seleccione dependerá completamente de usted, pero lo más probable es que esté basado en dos factores. El primer factor vendrá de las pruebas de los tiempos de llamada de las búsquedas. Averigüe en promedio cuánto tiempo toma y seleccione un valor basado en eso con un poco de protección solo para estar seguro. El segundo factor es el tiempo que una persona en el otro extremo está dispuesta a esperar por sus resultados. Podría ser muy conservador en su tiempo de espera, lo que hace que transcurran unos 60 segundos para estar seguro, pero si de hecho hay alguien en el otro extremo esperando los resultados, ¿cuánto tiempo estarán dispuestos a esperar? Prefiero obtener una respuesta de error que indique que debo intentarlo de nuevo en lugar de esperar para siempre. Por lo tanto, teniendo en cuenta estos dos factores, debe seleccionar un valor que le permita obtener respuestas un porcentaje muy alto del tiempo mientras no hace que la persona que llama en el otro extremo espere demasiado.
Para la pregunta 3, pregunte qué sucede si el mensaje se cae. En este caso, supongo que el futuro para quienquiera que reciba ese mensaje solo se agotará porque no recibirá una respuesta porque el actor receptor nunca recibirá un mensaje para responder. Akka no es JMS; no tiene modos de acuse de recibo donde un mensaje se puede reenviar varias veces si el destinatario no recibe y acepta el mensaje.
Además, como puede ver en mi ejemplo, estoy de acuerdo con no bloquear el Future
agregado mediante el uso de Await
. Prefiero usar los callbacks no bloqueantes. El bloqueo en una función de recepción no es ideal, ya que la instancia de Actor
dejará de procesar su buzón hasta que se complete la operación de bloqueo. Al utilizar una devolución de llamada no bloqueante, libera esa instancia para volver al procesamiento de su buzón y permite que el manejo del resultado sea simplemente otro trabajo que se ejecuta en el ExecutionContext
, desacoplado del actor que procesa su buzón.
Ahora, si realmente desea no desperdiciar las comunicaciones cuando la red no es confiable, puede buscar el Proxy confiable disponible en Akka 2.2. Si no desea seguir esta ruta, puede enrollarla usted mismo enviando periódicamente mensajes de tipo ping
a los destinatarios. Si uno no responde a tiempo, marque hacia abajo y no le envíe mensajes hasta que pueda obtener un ping
confiable (en un tiempo muy corto), como un FSM por enrutado. Cualquiera de estos puede funcionar si necesita absolutamente este comportamiento, pero debe recordar que estas soluciones agregan complejidad y solo deben emplearse si realmente necesita este comportamiento. Si está desarrollando software bancario y necesita absolutamente una semántica de entrega garantizada, ya que de lo contrario tendrían consecuencias financieras negativas, por lo menos con este tipo de enfoque. Solo sea juicioso al decidir si necesita algo como esto porque apuesto que el 90% del tiempo no lo hace. En tu modelo, la única persona probablemente afectada por esperar en algo que ya sabías que no tendrá éxito es la persona que llama en el otro extremo. Al utilizar devoluciones de llamada no bloqueantes en el actor, no se detiene por el hecho de que algo puede tardar mucho tiempo; Ya se ha movido en su siguiente mensaje. También debe tener cuidado si decide volver a enviar en caso de fallo. Usted no quiere inundar los buzones de los actores receptores. Si decide reenviar, tápelo en un número fijo de veces.
Otro posible enfoque si necesita este tipo de semántica garantizada podría ser examinar el modelo de agrupación en clúster de Akka. Si agrupaba los enrutados en sentido descendente y uno de los servidores fallaba, todo el tráfico se enrutaría al nodo que aún estaba activo hasta que ese otro nodo se recuperara.
He revisado algunas publicaciones sobre cómo y por qué akka no garantiza la entrega de mensajes. La documentation , esta discussion y las otras discusiones en grupo lo explican bien.
Soy bastante nuevo en akka y deseo saber el diseño apropiado para un caso. Por ejemplo, digamos que tengo 3 actores diferentes, todos en diferentes máquinas. Uno es responsable de los libros de cocina, el otro de la historia y el último de los libros de tecnología.
Tengo un actor principal en otra máquina. Supongamos que hay una consulta al actor principal para buscar si tenemos algún libro disponible. El actor principal envía solicitudes a los 3 actores remotos y espera el resultado. Así que hago esto:
val scatter = system.actorOf(
Props[SearchActor].withRouter(ScatterGatherFirstCompletedRouter(
routees=someRoutees, within = 10 seconds)), "router")
implicit val timeout = Timeout(10 seconds)
val futureResult = scatter ? Text("Concurrency in Practice")
// What should I do here?.
//val result = Await.result(futureResult, timeout.duration) line(a)
En resumen, he enviado solicitudes a los 3 actores remotos y espero el resultado en 10 segundos.
¿Cuál debería ser la acción?
- Digamos que no obtengo el resultado en 10 segundos, ¿debo enviar una nueva solicitud a todos ellos nuevamente?
- Lo que si
within
tiempo anterior es prematuro. Pero no sé de antemano cuánto tiempo puede llevar. - ¿Qué pasaría si
within
tiempo fuera suficiente pero el mensaje se cayó?
Si no obtengo respuesta within
tiempo y reenvío la solicitud nuevamente. Algo así, sigue siendo asíncrono:
futureResult onComplete{
case Success(i) => println("Result "+i)
case Failure(e) => //send again
}
Pero bajo demasiadas consultas, ¿no serán demasiados hilos en la llamada y voluminosos? Si descomento la line(a)
, se vuelve sincrónica y la carga insuficiente podría funcionar mal.
Digamos que no recibo respuesta en 10 segundos. Si within
tiempo fue prematuro, entonces es una pesada computación inútil que está sucediendo nuevamente. Si el mensaje se caía, desperdicia 10
segundos de tiempo valioso. En el caso, digamos que sabía que el mensaje se había entregado, probablemente esperaría por más tiempo sin ser escéptico.
¿Cómo resuelven las personas estos problemas? ¿ ACK
? Pero luego tengo que almacenar el estado en actor de todas las consultas. Debe ser algo común y estoy buscando el diseño correcto.