feat: キューがタイムアウトしたらそれを保存するように
This commit is contained in:
parent
1e57f1ef4b
commit
dcc1f582a9
|
@ -62,11 +62,13 @@ class MongodbQueuedTaskRepository(
|
||||||
val findOneAndUpdate = collection.findOneAndUpdate(
|
val findOneAndUpdate = collection.findOneAndUpdate(
|
||||||
and(
|
and(
|
||||||
eq("_id", id.toString()),
|
eq("_id", id.toString()),
|
||||||
eq(QueuedTaskMongodb::assignedConsumer.name, null)
|
eq(QueuedTaskMongodb::isActive.name, true)
|
||||||
),
|
),
|
||||||
listOf(
|
listOf(
|
||||||
set(QueuedTaskMongodb::assignedConsumer.name, update.assignedConsumer),
|
set(QueuedTaskMongodb::assignedConsumer.name, update.assignedConsumer),
|
||||||
set(QueuedTaskMongodb::assignedAt.name, update.assignedAt)
|
set(QueuedTaskMongodb::assignedAt.name, update.assignedAt),
|
||||||
|
set(QueuedTaskMongodb::queuedAt.name, update.queuedAt),
|
||||||
|
set(QueuedTaskMongodb::isActive.name, update.isActive)
|
||||||
),
|
),
|
||||||
FindOneAndUpdateOptions().upsert(false).returnDocument(ReturnDocument.AFTER)
|
FindOneAndUpdateOptions().upsert(false).returnDocument(ReturnDocument.AFTER)
|
||||||
)
|
)
|
||||||
|
@ -77,20 +79,25 @@ class MongodbQueuedTaskRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByTaskNameInAndAssignedConsumerIsNullAndOrderByPriority(
|
override fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(
|
||||||
tasks: List<String>,
|
tasks: List<String>,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Flow<QueuedTask> {
|
): Flow<QueuedTask> {
|
||||||
return collection.find<QueuedTaskMongodb>(
|
return collection.find<QueuedTaskMongodb>(
|
||||||
and(
|
and(
|
||||||
`in`("task.name", tasks),
|
`in`("task.name", tasks),
|
||||||
eq(QueuedTaskMongodb::assignedConsumer.name, null)
|
eq(QueuedTaskMongodb::isActive.name, true)
|
||||||
)
|
)
|
||||||
).map { it.toQueuedTask(propertySerializerFactory) }.flowOn(Dispatchers.IO)
|
).map { it.toQueuedTask(propertySerializerFactory) }.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByQueuedAtBeforeAndAssignedConsumerIsNull(instant: Instant): Flow<QueuedTask> {
|
override fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow<QueuedTask> {
|
||||||
return collection.find(lte(QueuedTaskMongodb::queuedAt.name, instant))
|
return collection.find(
|
||||||
|
and(
|
||||||
|
lte(QueuedTaskMongodb::queuedAt.name, instant),
|
||||||
|
eq(QueuedTaskMongodb::isActive.name, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
.map { it.toQueuedTask(propertySerializerFactory) }.flowOn(Dispatchers.IO)
|
.map { it.toQueuedTask(propertySerializerFactory) }.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +109,8 @@ data class QueuedTaskMongodb(
|
||||||
val task: TaskMongodb,
|
val task: TaskMongodb,
|
||||||
val attempt: Int,
|
val attempt: Int,
|
||||||
val queuedAt: Instant,
|
val queuedAt: Instant,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val timeoutAt: Instant?,
|
||||||
val assignedConsumer: String?,
|
val assignedConsumer: String?,
|
||||||
val assignedAt: Instant?
|
val assignedAt: Instant?
|
||||||
) {
|
) {
|
||||||
|
@ -111,6 +120,8 @@ data class QueuedTaskMongodb(
|
||||||
attempt,
|
attempt,
|
||||||
queuedAt,
|
queuedAt,
|
||||||
task.toTask(propertySerializerFactory),
|
task.toTask(propertySerializerFactory),
|
||||||
|
isActive,
|
||||||
|
timeoutAt,
|
||||||
assignedConsumer?.let { UUID.fromString(it) },
|
assignedConsumer?.let { UUID.fromString(it) },
|
||||||
assignedAt
|
assignedAt
|
||||||
)
|
)
|
||||||
|
@ -155,6 +166,7 @@ data class QueuedTaskMongodb(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun of(propertySerializerFactory: PropertySerializerFactory, queuedTask: QueuedTask): QueuedTaskMongodb {
|
fun of(propertySerializerFactory: PropertySerializerFactory, queuedTask: QueuedTask): QueuedTaskMongodb {
|
||||||
return QueuedTaskMongodb(
|
return QueuedTaskMongodb(
|
||||||
|
@ -162,6 +174,8 @@ data class QueuedTaskMongodb(
|
||||||
TaskMongodb.of(propertySerializerFactory, queuedTask.task),
|
TaskMongodb.of(propertySerializerFactory, queuedTask.task),
|
||||||
queuedTask.attempt,
|
queuedTask.attempt,
|
||||||
queuedTask.queuedAt,
|
queuedTask.queuedAt,
|
||||||
|
queuedTask.isActive,
|
||||||
|
queuedTask.timeoutAt,
|
||||||
queuedTask.assignedConsumer?.toString(),
|
queuedTask.assignedConsumer?.toString(),
|
||||||
queuedTask.assignedAt
|
queuedTask.assignedAt
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.singleOrNull
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.bson.BsonType
|
import org.bson.BsonType
|
||||||
import org.bson.codecs.pojo.annotations.BsonId
|
import org.bson.codecs.pojo.annotations.BsonId
|
||||||
|
@ -52,6 +53,10 @@ class MongodbTaskRepository(database: MongoDatabase, private val propertySeriali
|
||||||
return collection.find(Filters.lte(TaskMongodb::nextRetry.name, timestamp))
|
return collection.find(Filters.lte(TaskMongodb::nextRetry.name, timestamp))
|
||||||
.map { it.toTask(propertySerializerFactory) }.flowOn(Dispatchers.IO)
|
.map { it.toTask(propertySerializerFactory) }.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(uuid: UUID): Task? = withContext(Dispatchers.IO) {
|
||||||
|
collection.find(Filters.eq(uuid.toString())).singleOrNull()?.toTask(propertySerializerFactory)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TaskMongodb(
|
data class TaskMongodb(
|
||||||
|
|
|
@ -22,11 +22,14 @@ import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param attempt キューされた時点での試行回数より1多い
|
* @param attempt キューされた時点での試行回数より1多い
|
||||||
|
* @param isActive trueならアサイン可能 falseならアサイン済みかタイムアウト等で無効
|
||||||
*/
|
*/
|
||||||
data class QueuedTask(
|
data class QueuedTask(
|
||||||
val attempt: Int,
|
val attempt: Int,
|
||||||
val queuedAt: Instant,
|
val queuedAt: Instant,
|
||||||
val task: Task,
|
val task: Task,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val timeoutAt: Instant?,
|
||||||
val assignedConsumer: UUID?,
|
val assignedConsumer: UUID?,
|
||||||
val assignedAt: Instant?
|
val assignedAt: Instant?
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,7 +28,7 @@ interface QueuedTaskRepository {
|
||||||
*/
|
*/
|
||||||
suspend fun findByTaskIdAndAssignedConsumerIsNullAndUpdate(id:UUID,update:QueuedTask):QueuedTask
|
suspend fun findByTaskIdAndAssignedConsumerIsNullAndUpdate(id:UUID,update:QueuedTask):QueuedTask
|
||||||
|
|
||||||
fun findByTaskNameInAndAssignedConsumerIsNullAndOrderByPriority(tasks:List<String>,limit:Int): Flow<QueuedTask>
|
fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks: List<String>, limit: Int): Flow<QueuedTask>
|
||||||
|
|
||||||
fun findByQueuedAtBeforeAndAssignedConsumerIsNull(instant: Instant): Flow<QueuedTask>
|
fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow<QueuedTask>
|
||||||
}
|
}
|
|
@ -18,9 +18,12 @@ package dev.usbharu.owl.broker.domain.model.task
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
interface TaskRepository {
|
interface TaskRepository {
|
||||||
suspend fun save(task: Task):Task
|
suspend fun save(task: Task):Task
|
||||||
|
|
||||||
fun findByNextRetryBefore(timestamp:Instant): Flow<Task>
|
fun findByNextRetryBefore(timestamp:Instant): Flow<Task>
|
||||||
|
|
||||||
|
suspend fun findById(uuid: UUID): Task?
|
||||||
}
|
}
|
|
@ -55,7 +55,7 @@ class TaskPublishService(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
PublishedTask.newBuilder().setName(publishedTask.name).setId(publishedTask.id.toUUID()).build()
|
PublishedTask.newBuilder().setName(publishedTask.name).setId(publishedTask.id.toUUID()).build()
|
||||||
}catch (e:Error){
|
} catch (e: Throwable) {
|
||||||
logger.warn("exception ",e)
|
logger.warn("exception ",e)
|
||||||
throw StatusException(Status.INTERNAL)
|
throw StatusException(Status.INTERNAL)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ class AssignQueuedTaskDeciderImpl(
|
||||||
val consumer = consumerRepository.findById(consumerId)
|
val consumer = consumerRepository.findById(consumerId)
|
||||||
?: throw RecordNotFoundException("Consumer not found. id: $consumerId")
|
?: throw RecordNotFoundException("Consumer not found. id: $consumerId")
|
||||||
emitAll(
|
emitAll(
|
||||||
queueStore.findByTaskNameInAndAssignedConsumerIsNullAndOrderByPriority(
|
queueStore.findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(
|
||||||
consumer.tasks,
|
consumer.tasks,
|
||||||
numberOfConcurrent
|
numberOfConcurrent
|
||||||
).take(numberOfConcurrent)
|
).take(numberOfConcurrent)
|
||||||
|
|
|
@ -42,7 +42,7 @@ class QueueScannerImpl(private val queueStore: QueueStore) : QueueScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scanQueue(): Flow<QueuedTask> {
|
private fun scanQueue(): Flow<QueuedTask> {
|
||||||
return queueStore.findByQueuedAtBeforeAndAssignedConsumerIsNull(Instant.now().minusSeconds(10))
|
return queueStore.findByQueuedAtBeforeAndIsActiveIsTrue(Instant.now().minusSeconds(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -28,9 +28,9 @@ interface QueueStore {
|
||||||
|
|
||||||
suspend fun dequeue(queuedTask: QueuedTask)
|
suspend fun dequeue(queuedTask: QueuedTask)
|
||||||
suspend fun dequeueAll(queuedTaskList: List<QueuedTask>)
|
suspend fun dequeueAll(queuedTaskList: List<QueuedTask>)
|
||||||
fun findByTaskNameInAndAssignedConsumerIsNullAndOrderByPriority(tasks: List<String>, limit: Int): Flow<QueuedTask>
|
fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks: List<String>, limit: Int): Flow<QueuedTask>
|
||||||
|
|
||||||
fun findByQueuedAtBeforeAndAssignedConsumerIsNull(instant: Instant): Flow<QueuedTask>
|
fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow<QueuedTask>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@ -51,15 +51,15 @@ class QueueStoreImpl(private val queuedTaskRepository: QueuedTaskRepository) : Q
|
||||||
return queuedTaskList.forEach { dequeue(it) }
|
return queuedTaskList.forEach { dequeue(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByTaskNameInAndAssignedConsumerIsNullAndOrderByPriority(
|
override fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(
|
||||||
tasks: List<String>,
|
tasks: List<String>,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Flow<QueuedTask> {
|
): Flow<QueuedTask> {
|
||||||
return queuedTaskRepository.findByTaskNameInAndAssignedConsumerIsNullAndOrderByPriority(tasks, limit)
|
return queuedTaskRepository.findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByQueuedAtBeforeAndAssignedConsumerIsNull(instant: Instant): Flow<QueuedTask> {
|
override fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow<QueuedTask> {
|
||||||
return queuedTaskRepository.findByQueuedAtBeforeAndAssignedConsumerIsNull(instant)
|
return queuedTaskRepository.findByQueuedAtBeforeAndIsActiveIsTrue(instant)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -53,8 +53,14 @@ class QueuedTaskAssignerImpl(
|
||||||
private suspend fun assignTask(queuedTask: QueuedTask, consumerId: UUID): QueuedTask? {
|
private suspend fun assignTask(queuedTask: QueuedTask, consumerId: UUID): QueuedTask? {
|
||||||
return try {
|
return try {
|
||||||
|
|
||||||
val assignedTaskQueue = queuedTask.copy(assignedConsumer = consumerId, assignedAt = Instant.now())
|
val assignedTaskQueue =
|
||||||
logger.trace("Try assign task: {} id: {} consumer: {}",queuedTask.task.name,queuedTask.task.id,consumerId)
|
queuedTask.copy(assignedConsumer = consumerId, assignedAt = Instant.now(), isActive = false)
|
||||||
|
logger.trace(
|
||||||
|
"Try assign task: {} id: {} consumer: {}",
|
||||||
|
queuedTask.task.name,
|
||||||
|
queuedTask.task.id,
|
||||||
|
consumerId
|
||||||
|
)
|
||||||
|
|
||||||
queueStore.dequeue(assignedTaskQueue)
|
queueStore.dequeue(assignedTaskQueue)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package dev.usbharu.owl.broker.service
|
package dev.usbharu.owl.broker.service
|
||||||
|
|
||||||
|
import dev.usbharu.owl.broker.domain.exception.repository.RecordNotFoundException
|
||||||
import dev.usbharu.owl.broker.domain.exception.service.TaskNotRegisterException
|
import dev.usbharu.owl.broker.domain.exception.service.TaskNotRegisterException
|
||||||
import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTask
|
import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTask
|
||||||
import dev.usbharu.owl.broker.domain.model.task.Task
|
import dev.usbharu.owl.broker.domain.model.task.Task
|
||||||
|
@ -67,12 +68,7 @@ class TaskManagementServiceImpl(
|
||||||
},
|
},
|
||||||
launch {
|
launch {
|
||||||
queueFlow.onEach {
|
queueFlow.onEach {
|
||||||
logger.warn(
|
timeoutQueue(it)
|
||||||
"Queue timed out. name: {} id: {} attempt: {}",
|
|
||||||
it.task.name,
|
|
||||||
it.task.id,
|
|
||||||
it.attempt
|
|
||||||
)
|
|
||||||
}.collect()
|
}.collect()
|
||||||
}
|
}
|
||||||
).joinAll()
|
).joinAll()
|
||||||
|
@ -90,6 +86,8 @@ class TaskManagementServiceImpl(
|
||||||
task.attempt + 1,
|
task.attempt + 1,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
task,
|
task,
|
||||||
|
isActive = true,
|
||||||
|
timeoutAt = null,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
@ -98,7 +96,7 @@ class TaskManagementServiceImpl(
|
||||||
?: throw TaskNotRegisterException("Task ${task.name} not definition.")
|
?: throw TaskNotRegisterException("Task ${task.name} not definition.")
|
||||||
val copy = task.copy(
|
val copy = task.copy(
|
||||||
nextRetry = retryPolicyFactory.factory(definedTask.retryPolicy)
|
nextRetry = retryPolicyFactory.factory(definedTask.retryPolicy)
|
||||||
.nextRetry(Instant.now(), task.attempt)
|
.nextRetry(Instant.now(), queuedTask.attempt)
|
||||||
)
|
)
|
||||||
|
|
||||||
taskRepository.save(copy)
|
taskRepository.save(copy)
|
||||||
|
@ -108,6 +106,26 @@ class TaskManagementServiceImpl(
|
||||||
return queuedTask
|
return queuedTask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun timeoutQueue(queuedTask: QueuedTask) {
|
||||||
|
val timeoutQueue = queuedTask.copy(isActive = false, timeoutAt = Instant.now())
|
||||||
|
|
||||||
|
queueStore.dequeue(timeoutQueue)
|
||||||
|
|
||||||
|
|
||||||
|
val task = taskRepository.findById(timeoutQueue.task.id)
|
||||||
|
?: throw RecordNotFoundException("Task not found. id: ${timeoutQueue.task.id}")
|
||||||
|
val copy = task.copy(attempt = timeoutQueue.attempt)
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
"Queue timed out. name: {} id: {} attempt: {}",
|
||||||
|
timeoutQueue.task.name,
|
||||||
|
timeoutQueue.task.id,
|
||||||
|
timeoutQueue.attempt
|
||||||
|
)
|
||||||
|
taskRepository.save(copy)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(TaskManagementServiceImpl::class.java)
|
private val logger = LoggerFactory.getLogger(TaskManagementServiceImpl::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ class TaskPublishServiceImpl(
|
||||||
?: throw TaskNotRegisterException("Task ${publishTask.name} not definition.")
|
?: throw TaskNotRegisterException("Task ${publishTask.name} not definition.")
|
||||||
|
|
||||||
val published = Instant.now()
|
val published = Instant.now()
|
||||||
val nextRetry = retryPolicyFactory.factory(definition.name).nextRetry(published,0)
|
val nextRetry = retryPolicyFactory.factory(definition.retryPolicy).nextRetry(published, 0)
|
||||||
|
|
||||||
val task = Task(
|
val task = Task(
|
||||||
name = publishTask.name,
|
name = publishTask.name,
|
||||||
|
|
|
@ -6,6 +6,6 @@ import kotlin.math.roundToLong
|
||||||
|
|
||||||
class ExponentialRetryPolicy(private val firstRetrySeconds: Int = 30) : RetryPolicy {
|
class ExponentialRetryPolicy(private val firstRetrySeconds: Int = 30) : RetryPolicy {
|
||||||
override fun nextRetry(now: Instant, attempt: Int): Instant =
|
override fun nextRetry(now: Instant, attempt: Int): Instant =
|
||||||
now.plusSeconds(firstRetrySeconds.times((2.0).pow(attempt).roundToLong()))
|
now.plusSeconds(firstRetrySeconds.times((2.0).pow(attempt).roundToLong()) - 30)
|
||||||
|
|
||||||
}
|
}
|
|
@ -25,12 +25,12 @@ class ExponentialRetryPolicyTest {
|
||||||
fun exponential0() {
|
fun exponential0() {
|
||||||
val nextRetry = ExponentialRetryPolicy().nextRetry(Instant.ofEpochSecond(300), 0)
|
val nextRetry = ExponentialRetryPolicy().nextRetry(Instant.ofEpochSecond(300), 0)
|
||||||
|
|
||||||
assertEquals(Instant.ofEpochSecond(330), nextRetry)
|
assertEquals(Instant.ofEpochSecond(300), nextRetry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun exponential1() {
|
fun exponential1() {
|
||||||
val nextRetry = ExponentialRetryPolicy().nextRetry(Instant.ofEpochSecond(300), 1)
|
val nextRetry = ExponentialRetryPolicy().nextRetry(Instant.ofEpochSecond(300), 1)
|
||||||
assertEquals(Instant.ofEpochSecond(360), nextRetry)
|
assertEquals(Instant.ofEpochSecond(330), nextRetry)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue