1
1
package com .avast .clients .rabbitmq
2
2
3
3
import cats .Applicative
4
- import cats .effect .{Resource , Sync }
4
+ import cats .effect .{Resource , Sync , Timer }
5
5
import cats .implicits .{catsSyntaxApplicativeError , catsSyntaxFlatMapOps , toFunctorOps }
6
6
import com .avast .bytes .Bytes
7
7
import com .avast .clients .rabbitmq .PoisonedMessageHandler .{defaultHandlePoisonedMessage , DiscardedTimeHeaderName }
@@ -19,7 +19,8 @@ sealed trait PoisonedMessageHandler[F[_], A] {
19
19
implicit dctx : DeliveryContext ): F [DeliveryResult ]
20
20
}
21
21
22
- class LoggingPoisonedMessageHandler [F [_]: Sync , A ](maxAttempts : Int ) extends PoisonedMessageHandler [F , A ] {
22
+ class LoggingPoisonedMessageHandler [F [_]: Sync : Timer , A ](maxAttempts : Int , republishDelay : Option [ExponentialDelay ])
23
+ extends PoisonedMessageHandler [F , A ] {
23
24
private val logger = ImplicitContextLogger .createLogger[F , LoggingPoisonedMessageHandler [F , A ]]
24
25
25
26
override def interceptResult (delivery : Delivery [A ], messageId : MessageId , rawBody : Bytes )(result : DeliveryResult )(
@@ -28,6 +29,7 @@ class LoggingPoisonedMessageHandler[F[_]: Sync, A](maxAttempts: Int) extends Poi
28
29
messageId,
29
30
maxAttempts,
30
31
logger,
32
+ republishDelay,
31
33
(d : Delivery [A ], _) => defaultHandlePoisonedMessage[F , A ](maxAttempts, logger)(d))(result)
32
34
}
33
35
}
@@ -37,14 +39,19 @@ class NoOpPoisonedMessageHandler[F[_]: Sync, A] extends PoisonedMessageHandler[F
37
39
implicit dctx : DeliveryContext ): F [DeliveryResult ] = Sync [F ].pure(result)
38
40
}
39
41
40
- class DeadQueuePoisonedMessageHandler [F [_]: Sync , A ](maxAttempts : Int )(moveToDeadQueue : (Delivery [A ], Bytes , DeliveryContext ) => F [Unit ])
42
+ class DeadQueuePoisonedMessageHandler [F [_]: Sync : Timer , A ](maxAttempts : Int , republishDelay : Option [ExponentialDelay ])(
43
+ moveToDeadQueue : (Delivery [A ], Bytes , DeliveryContext ) => F [Unit ])
41
44
extends PoisonedMessageHandler [F , A ] {
42
45
private val logger = ImplicitContextLogger .createLogger[F , DeadQueuePoisonedMessageHandler [F , A ]]
43
46
44
47
override def interceptResult (delivery : Delivery [A ], messageId : MessageId , rawBody : Bytes )(result : DeliveryResult )(
45
48
implicit dctx : DeliveryContext ): F [DeliveryResult ] = {
46
- PoisonedMessageHandler .handleResult(delivery, messageId, maxAttempts, logger, (d, _) => handlePoisonedMessage(d, messageId, rawBody))(
47
- result)
49
+ PoisonedMessageHandler .handleResult(delivery,
50
+ messageId,
51
+ maxAttempts,
52
+ logger,
53
+ republishDelay,
54
+ (d, _) => handlePoisonedMessage(d, messageId, rawBody))(result)
48
55
}
49
56
50
57
private def handlePoisonedMessage (delivery : Delivery [A ], messageId : MessageId , rawBody : Bytes )(
@@ -59,9 +66,9 @@ class DeadQueuePoisonedMessageHandler[F[_]: Sync, A](maxAttempts: Int)(moveToDea
59
66
}
60
67
61
68
object DeadQueuePoisonedMessageHandler {
62
- def make [F [_]: Sync , A ](c : DeadQueuePoisonedMessageHandling ,
63
- connection : RabbitMQConnection [F ],
64
- monitor : Monitor [F ]): Resource [F , DeadQueuePoisonedMessageHandler [F , A ]] = {
69
+ def make [F [_]: Sync : Timer , A ](c : DeadQueuePoisonedMessageHandling ,
70
+ connection : RabbitMQConnection [F ],
71
+ monitor : Monitor [F ]): Resource [F , DeadQueuePoisonedMessageHandler [F , A ]] = {
65
72
val dqpc = c.deadQueueProducer
66
73
val pc = ProducerConfig (
67
74
name = dqpc.name,
@@ -73,7 +80,7 @@ object DeadQueuePoisonedMessageHandler {
73
80
)
74
81
75
82
connection.newProducer[Bytes ](pc, monitor.named(" deadQueueProducer" )).map { producer =>
76
- new DeadQueuePoisonedMessageHandler [F , A ](c.maxAttempts)((d : Delivery [A ], rawBody : Bytes , dctx : DeliveryContext ) => {
83
+ new DeadQueuePoisonedMessageHandler [F , A ](c.maxAttempts, c.republishDelay )((d : Delivery [A ], rawBody : Bytes , dctx : DeliveryContext ) => {
77
84
val cidStrategy = dctx.correlationId match {
78
85
case Some (value) => CorrelationIdStrategy .Fixed (value.value)
79
86
case None => CorrelationIdStrategy .RandomNew
@@ -93,11 +100,12 @@ object PoisonedMessageHandler {
93
100
final val RepublishCountHeaderName : String = " X-Republish-Count"
94
101
final val DiscardedTimeHeaderName : String = " X-Discarded-Time"
95
102
96
- private [rabbitmq] def make [F [_]: Sync , A ](config : Option [PoisonedMessageHandlingConfig ],
97
- connection : RabbitMQConnection [F ],
98
- monitor : Monitor [F ]): Resource [F , PoisonedMessageHandler [F , A ]] = {
103
+ private [rabbitmq] def make [F [_]: Sync : Timer , A ](config : Option [PoisonedMessageHandlingConfig ],
104
+ connection : RabbitMQConnection [F ],
105
+ monitor : Monitor [F ]): Resource [F , PoisonedMessageHandler [F , A ]] = {
99
106
config match {
100
- case Some (LoggingPoisonedMessageHandling (maxAttempts)) => Resource .pure(new LoggingPoisonedMessageHandler [F , A ](maxAttempts))
107
+ case Some (LoggingPoisonedMessageHandling (maxAttempts, republishDelay)) =>
108
+ Resource .pure(new LoggingPoisonedMessageHandler [F , A ](maxAttempts, republishDelay))
101
109
case Some (c : DeadQueuePoisonedMessageHandling ) => DeadQueuePoisonedMessageHandler .make(c, connection, monitor)
102
110
case Some (NoOpPoisonedMessageHandling ) | None =>
103
111
Resource .eval {
@@ -114,26 +122,30 @@ object PoisonedMessageHandler {
114
122
logger.warn(s " Message failures reached the limit $maxAttempts attempts, throwing away: $delivery" )
115
123
}
116
124
117
- private [rabbitmq] def handleResult [F [_]: Sync , A ](
125
+ private [rabbitmq] def handleResult [F [_]: Sync : Timer , A ](
118
126
delivery : Delivery [A ],
119
127
messageId : MessageId ,
120
128
maxAttempts : Int ,
121
129
logger : ImplicitContextLogger [F ],
130
+ republishDelay : Option [ExponentialDelay ],
122
131
handlePoisonedMessage : (Delivery [A ], Int ) => F [Unit ])(r : DeliveryResult )(implicit dctx : DeliveryContext ): F [DeliveryResult ] = {
123
132
r match {
124
133
case Republish (isPoisoned, newHeaders) if isPoisoned =>
125
- adjustDeliveryResult(delivery, messageId, maxAttempts, newHeaders, logger, handlePoisonedMessage)
134
+ adjustDeliveryResult(delivery, messageId, maxAttempts, newHeaders, logger, republishDelay, handlePoisonedMessage)
126
135
case r => Applicative [F ].pure(r) // keep other results as they are
127
136
}
128
137
}
129
138
130
- private def adjustDeliveryResult [F [_]: Sync , A ](
139
+ private def adjustDeliveryResult [F [_]: Sync : Timer , A ](
131
140
delivery : Delivery [A ],
132
141
messageId : MessageId ,
133
142
maxAttempts : Int ,
134
143
newHeaders : Map [String , AnyRef ],
135
144
logger : ImplicitContextLogger [F ],
145
+ republishDelay : Option [ExponentialDelay ],
136
146
handlePoisonedMessage : (Delivery [A ], Int ) => F [Unit ])(implicit dctx : DeliveryContext ): F [DeliveryResult ] = {
147
+ import cats .syntax .traverse ._
148
+
137
149
// get current attempt no. from passed headers with fallback to original (incoming) headers - the fallback will most likely happen
138
150
// but we're giving the programmer chance to programmatically _pretend_ lower attempt number
139
151
val attempt = (delivery.properties.headers ++ newHeaders)
@@ -143,8 +155,12 @@ object PoisonedMessageHandler {
143
155
144
156
logger.debug(s " Attempt $attempt/ $maxAttempts for $messageId" ) >> {
145
157
if (attempt < maxAttempts) {
146
- Applicative [F ].pure(
147
- Republish (countAsPoisoned = true , newHeaders = newHeaders + (RepublishCountHeaderName -> attempt.asInstanceOf [AnyRef ])))
158
+ for {
159
+ _ <- republishDelay.traverse { d =>
160
+ val delay = d.getExponentialDelay(attempt)
161
+ logger.debug(s " Will republish the message in $delay" ) >> Timer [F ].sleep(delay)
162
+ }
163
+ } yield Republish (countAsPoisoned = true , newHeaders = newHeaders + (RepublishCountHeaderName -> attempt.asInstanceOf [AnyRef ]))
148
164
} else {
149
165
val now = Instant .now()
150
166
0 commit comments