Move job queue to scheduler-api and fix notification of periodic tasks

This commit is contained in:
eikek
2022-03-12 15:31:27 +01:00
parent aafd908906
commit 83d3644b39
31 changed files with 108 additions and 103 deletions

View File

@ -0,0 +1,22 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.scheduler.impl
sealed trait Marked[+A] {}
object Marked {
final case class Found[A](value: A) extends Marked[A]
final case object NotFound extends Marked[Nothing]
final case object NotMarkable extends Marked[Nothing]
def found[A](v: A): Marked[A] = Found(v)
def notFound[A]: Marked[A] = NotFound
def notMarkable[A]: Marked[A] = NotMarkable
}

View File

@ -1,8 +1,8 @@
package docspell.scheduler.impl
import cats.effect._
import docspell.pubsub.api.PubSubT
import docspell.scheduler._
import docspell.store.queue.{JobQueue, PeriodicTaskStore}
import fs2.concurrent.SignallingRef
object PeriodicSchedulerBuilder {
@ -12,7 +12,7 @@ object PeriodicSchedulerBuilder {
sch: Scheduler[F],
queue: JobQueue[F],
store: PeriodicTaskStore[F],
notifyJoex: F[Unit]
pubsub: PubSubT[F]
): Resource[F, PeriodicScheduler[F]] =
for {
waiter <- Resource.eval(SignallingRef(true))
@ -22,7 +22,7 @@ object PeriodicSchedulerBuilder {
sch,
queue,
store,
notifyJoex,
pubsub,
waiter,
state
)

View File

@ -10,13 +10,12 @@ import cats.effect._
import cats.implicits._
import fs2._
import fs2.concurrent.SignallingRef
import docspell.common._
import docspell.pubsub.api.PubSubT
import docspell.scheduler._
import docspell.scheduler.impl.PeriodicSchedulerImpl.State
import docspell.store.queue._
import docspell.scheduler.msg.{JobsNotify, PeriodicTaskNotify}
import docspell.store.records.RPeriodicTask
import eu.timepit.fs2cron.calev.CalevScheduler
final class PeriodicSchedulerImpl[F[_]: Async](
@ -24,7 +23,7 @@ final class PeriodicSchedulerImpl[F[_]: Async](
sch: Scheduler[F],
queue: JobQueue[F],
store: PeriodicTaskStore[F],
joexNotifyAll: F[Unit],
pubSub: PubSubT[F],
waiter: SignallingRef[F, Boolean],
state: SignallingRef[F, State[F]]
) extends PeriodicScheduler[F] {
@ -49,6 +48,13 @@ final class PeriodicSchedulerImpl[F[_]: Async](
def notifyChange: F[Unit] =
waiter.update(b => !b)
def startSubscriptions: F[Unit] =
for {
_ <- Async[F].start(pubSub.subscribeSink(PeriodicTaskNotify()) { _ =>
logger.info("Notify periodic scheduler from message") *> notifyChange
})
} yield ()
// internal
/** On startup, get all periodic jobs from this scheduler and remove the mark, so they
@ -117,7 +123,7 @@ final class PeriodicSchedulerImpl[F[_]: Async](
}
def notifyJoex: F[Unit] =
sch.notifyChange *> joexNotifyAll
sch.notifyChange *> pubSub.publish1IgnoreErrors(JobsNotify(), ()).void
def scheduleNotify(pj: RPeriodicTask): F[Unit] =
Timestamp

View File

@ -0,0 +1,120 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.scheduler.impl
import cats.effect._
import cats.implicits._
import docspell.common._
import docspell.store.queries.QPeriodicTask
import docspell.store.records._
import docspell.store.{AddResult, Store}
trait PeriodicTaskStore[F[_]] {
/** Get the free periodic task due next and reserve it to the given worker.
*
* If found, the task is returned and resource finalization takes care of unmarking the
* task after use and updating `nextRun` with the next timestamp.
*/
def takeNext(
worker: Ident,
excludeId: Option[Ident]
): Resource[F, Marked[RPeriodicTask]]
def clearMarks(name: Ident): F[Unit]
def findNonFinalJob(pjobId: Ident): F[Option[RJob]]
/** Insert a task or update if it already exists. */
def insert(task: RPeriodicTask): F[Unit]
/** Adds the task only if it not already exists. */
def add(task: RPeriodicTask): F[AddResult]
/** Find all joex nodes as registered in the database. */
def findJoexNodes: F[Vector[RNode]]
}
object PeriodicTaskStore {
def create[F[_]: Sync](store: Store[F]): Resource[F, PeriodicTaskStore[F]] =
Resource.pure[F, PeriodicTaskStore[F]](new PeriodicTaskStore[F] {
private[this] val logger = docspell.logging.getLogger[F]
def takeNext(
worker: Ident,
excludeId: Option[Ident]
): Resource[F, Marked[RPeriodicTask]] = {
val chooseNext: F[Marked[RPeriodicTask]] =
getNext(excludeId).flatMap {
case Some(pj) =>
mark(pj.id, worker).map {
case true => Marked.found(pj.copy(worker = worker.some))
case false => Marked.notMarkable
}
case None =>
Marked.notFound[RPeriodicTask].pure[F]
}
Resource.make(chooseNext) {
case Marked.Found(pj) => unmark(pj)
case _ => ().pure[F]
}
}
def getNext(excl: Option[Ident]): F[Option[RPeriodicTask]] =
store.transact(QPeriodicTask.findNext(excl))
def mark(pid: Ident, name: Ident): F[Boolean] =
Timestamp
.current[F]
.flatMap(now =>
store.transact(QPeriodicTask.setWorker(pid, name, now)).map(_ > 0)
)
def unmark(job: RPeriodicTask): F[Unit] =
for {
now <- Timestamp.current[F]
nextRun = job.timer.nextElapse(now.atUTC).map(Timestamp.from)
_ <- store.transact(QPeriodicTask.unsetWorker(job.id, nextRun))
} yield ()
def clearMarks(name: Ident): F[Unit] =
store
.transact(QPeriodicTask.clearWorkers(name))
.flatMap { n =>
if (n > 0) logger.info(s"Clearing $n periodic tasks from worker ${name.id}")
else ().pure[F]
}
def findNonFinalJob(pjobId: Ident): F[Option[RJob]] =
store.transact(RJob.findNonFinalByTracker(pjobId))
def insert(task: RPeriodicTask): F[Unit] = {
val update = store.transact(RPeriodicTask.update(task))
val insertAttempt = store.transact(RPeriodicTask.insert(task)).attempt.map {
case Right(n) => n > 0
case Left(_) => false
}
for {
n1 <- update
ins <- if (n1 == 0) insertAttempt else true.pure[F]
_ <- if (ins) 1.pure[F] else update
} yield ()
}
def add(task: RPeriodicTask): F[AddResult] = {
val insert = RPeriodicTask.insert(task)
val exists = RPeriodicTask.exists(task.id)
store.add(insert, exists)
}
def findJoexNodes: F[Vector[RNode]] =
store.transact(RNode.findAll(NodeType.Joex))
})
}

View File

@ -10,12 +10,10 @@ import cats.effect._
import cats.effect.std.Semaphore
import cats.implicits._
import fs2.concurrent.SignallingRef
import docspell.scheduler._
import docspell.scheduler.{JobQueue, _}
import docspell.notification.api.EventSink
import docspell.pubsub.api.PubSubT
import docspell.store.Store
import docspell.store.queue.JobQueue
case class SchedulerBuilder[F[_]: Async](
config: SchedulerConfig,
@ -88,7 +86,7 @@ object SchedulerBuilder {
config,
JobTaskRegistry.empty[F],
store,
JobQueue(store),
JobQueue.create(store),
LogSink.db[F](store),
PubSubT.noop[F],
EventSink.silent[F]

View File

@ -12,19 +12,16 @@ import cats.effect.std.Semaphore
import cats.implicits._
import fs2.Stream
import fs2.concurrent.SignallingRef
import docspell.scheduler.msg.JobDone
import docspell.scheduler.msg.{CancelJob, JobDone, JobsNotify}
import docspell.common._
import docspell.scheduler._
import docspell.scheduler.{JobQueue, _}
import docspell.scheduler.impl.SchedulerImpl._
import docspell.notification.api.Event
import docspell.notification.api.EventSink
import docspell.pubsub.api.PubSubT
import docspell.store.Store
import docspell.store.queries.QJob
import docspell.store.queue.JobQueue
import docspell.store.records.RJob
import io.circe.Json
final class SchedulerImpl[F[_]: Async](
@ -42,6 +39,16 @@ final class SchedulerImpl[F[_]: Async](
private[this] val logger = docspell.logging.getLogger[F]
def startSubscriptions =
for {
_ <- Async[F].start(pubSub.subscribeSink(JobsNotify()) { _ =>
notifyChange
})
_ <- Async[F].start(pubSub.subscribeSink(CancelJob.topic) { msg =>
requestCancel(msg.body.jobId).void
})
} yield ()
/** On startup, get all jobs in state running from this scheduler and put them into
* waiting state, so they get picked up again.
*/