mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-11-04 12:30:12 +00:00 
			
		
		
		
	Sketch a scheduler for running periodic tasks
Periodic tasks are special in that they are usually kept around and started based on a schedule. A new component checks periodic tasks and submits them in the queue once they are due. In order to avoid duplicate periodic jobs, the tracker of a job is used to store the periodic job id. Each time a periodic task is due, it is first checked if there is a job running (or queued) for this task.
This commit is contained in:
		@@ -3,7 +3,7 @@ version = "2.4.2"
 | 
				
			|||||||
align = more
 | 
					align = more
 | 
				
			||||||
#align.arrowEnumeratorGenerator = true
 | 
					#align.arrowEnumeratorGenerator = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
maxColumn = 100
 | 
					maxColumn = 90
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rewrite.rules = [
 | 
					rewrite.rules = [
 | 
				
			||||||
  AvoidInfix
 | 
					  AvoidInfix
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -194,7 +194,8 @@ val store = project.in(file("modules/store")).
 | 
				
			|||||||
      Dependencies.databases ++
 | 
					      Dependencies.databases ++
 | 
				
			||||||
      Dependencies.flyway ++
 | 
					      Dependencies.flyway ++
 | 
				
			||||||
      Dependencies.loggingApi ++
 | 
					      Dependencies.loggingApi ++
 | 
				
			||||||
      Dependencies.emil
 | 
					      Dependencies.emil ++
 | 
				
			||||||
 | 
					      Dependencies.calev
 | 
				
			||||||
  ).dependsOn(common)
 | 
					  ).dependsOn(common)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
val extract = project.in(file("modules/extract")).
 | 
					val extract = project.in(file("modules/extract")).
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								modules/common/src/main/scala/docspell/common/Hash.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								modules/common/src/main/scala/docspell/common/Hash.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					package docspell.common
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import scodec.bits.ByteVector
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class Hash(bytes: ByteVector) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def digest(name: String): String =
 | 
				
			||||||
 | 
					    bytes.digest(name).toHex.toLowerCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def sha256: String =
 | 
				
			||||||
 | 
					    digest("SHA-256")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def md5: String =
 | 
				
			||||||
 | 
					    digest("MD5")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def add(str: String): Hash =
 | 
				
			||||||
 | 
					    new Hash(bytes ++ ByteVector.view(str.getBytes(StandardCharsets.UTF_8)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def add(id: Ident): Hash =
 | 
				
			||||||
 | 
					    add(id.id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object Hash {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def empty: Hash = new Hash(ByteVector.empty)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -4,6 +4,8 @@ import java.time.{Instant, LocalDate, ZoneId}
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import cats.effect.Sync
 | 
					import cats.effect.Sync
 | 
				
			||||||
import io.circe.{Decoder, Encoder}
 | 
					import io.circe.{Decoder, Encoder}
 | 
				
			||||||
 | 
					import java.time.LocalDateTime
 | 
				
			||||||
 | 
					import java.time.ZonedDateTime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class Timestamp(value: Instant) {
 | 
					case class Timestamp(value: Instant) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -17,13 +19,20 @@ case class Timestamp(value: Instant) {
 | 
				
			|||||||
  def minusHours(n: Long): Timestamp =
 | 
					  def minusHours(n: Long): Timestamp =
 | 
				
			||||||
    Timestamp(value.minusSeconds(n * 60 * 60))
 | 
					    Timestamp(value.minusSeconds(n * 60 * 60))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def toDate: LocalDate =
 | 
					  def toUtcDate: LocalDate =
 | 
				
			||||||
    value.atZone(ZoneId.of("UTC")).toLocalDate
 | 
					    value.atZone(Timestamp.UTC).toLocalDate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def toUtcDateTime: LocalDateTime =
 | 
				
			||||||
 | 
					    value.atZone(Timestamp.UTC).toLocalDateTime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def atZone(zone: ZoneId): ZonedDateTime =
 | 
				
			||||||
 | 
					    value.atZone(zone)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def asString: String = value.toString
 | 
					  def asString: String = value.toString
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Timestamp {
 | 
					object Timestamp {
 | 
				
			||||||
 | 
					  val UTC = ZoneId.of("UTC")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val Epoch = Timestamp(Instant.EPOCH)
 | 
					  val Epoch = Timestamp(Instant.EPOCH)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,6 +64,19 @@ docspell.joex {
 | 
				
			|||||||
    wakeup-period = "30 minutes"
 | 
					    wakeup-period = "30 minutes"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  periodic-scheduler {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Each scheduler needs a unique name. This defaults to the node
 | 
				
			||||||
 | 
					    # name, which must be unique, too.
 | 
				
			||||||
 | 
					    name = ${docspell.joex.app-id}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # A fallback to start looking for due periodic tasks regularily.
 | 
				
			||||||
 | 
					    # Usually joex instances should be notified via REST calls if
 | 
				
			||||||
 | 
					    # external processes change tasks. But these requests may get
 | 
				
			||||||
 | 
					    # lost.
 | 
				
			||||||
 | 
					    wakeup-period = "10 minutes"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Configuration of text extraction
 | 
					  # Configuration of text extraction
 | 
				
			||||||
  extraction {
 | 
					  extraction {
 | 
				
			||||||
    # For PDF files it is first tried to read the text parts of the
 | 
					    # For PDF files it is first tried to read the text parts of the
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
package docspell.joex
 | 
					package docspell.joex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import docspell.common.{Ident, LenientUri}
 | 
					import docspell.common.{Ident, LenientUri}
 | 
				
			||||||
import docspell.joex.scheduler.SchedulerConfig
 | 
					import docspell.joex.scheduler.{PeriodicSchedulerConfig, SchedulerConfig}
 | 
				
			||||||
import docspell.store.JdbcConfig
 | 
					import docspell.store.JdbcConfig
 | 
				
			||||||
import docspell.convert.ConvertConfig
 | 
					import docspell.convert.ConvertConfig
 | 
				
			||||||
import docspell.extract.ExtractConfig
 | 
					import docspell.extract.ExtractConfig
 | 
				
			||||||
@@ -12,6 +12,7 @@ case class Config(
 | 
				
			|||||||
    bind: Config.Bind,
 | 
					    bind: Config.Bind,
 | 
				
			||||||
    jdbc: JdbcConfig,
 | 
					    jdbc: JdbcConfig,
 | 
				
			||||||
    scheduler: SchedulerConfig,
 | 
					    scheduler: SchedulerConfig,
 | 
				
			||||||
 | 
					    periodicScheduler: PeriodicSchedulerConfig,
 | 
				
			||||||
    extraction: ExtractConfig,
 | 
					    extraction: ExtractConfig,
 | 
				
			||||||
    convert: ConvertConfig
 | 
					    convert: ConvertConfig
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
package docspell.joex
 | 
					package docspell.joex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import docspell.common.Ident
 | 
					import docspell.common.Ident
 | 
				
			||||||
import docspell.joex.scheduler.Scheduler
 | 
					import docspell.joex.scheduler.{PeriodicScheduler, Scheduler}
 | 
				
			||||||
import docspell.store.records.RJobLog
 | 
					import docspell.store.records.RJobLog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait JoexApp[F[_]] {
 | 
					trait JoexApp[F[_]] {
 | 
				
			||||||
@@ -10,6 +10,8 @@ trait JoexApp[F[_]] {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def scheduler: Scheduler[F]
 | 
					  def scheduler: Scheduler[F]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def periodicScheduler: PeriodicScheduler[F]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def findLogs(jobId: Ident): F[Vector[RJobLog]]
 | 
					  def findLogs(jobId: Ident): F[Vector[RJobLog]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Shuts down the job executor.
 | 
					  /** Shuts down the job executor.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,9 +3,11 @@ package docspell.joex
 | 
				
			|||||||
import cats.implicits._
 | 
					import cats.implicits._
 | 
				
			||||||
import cats.effect._
 | 
					import cats.effect._
 | 
				
			||||||
import docspell.common.{Ident, NodeType, ProcessItemArgs}
 | 
					import docspell.common.{Ident, NodeType, ProcessItemArgs}
 | 
				
			||||||
 | 
					import docspell.joex.background._
 | 
				
			||||||
import docspell.joex.process.ItemHandler
 | 
					import docspell.joex.process.ItemHandler
 | 
				
			||||||
import docspell.joex.scheduler.{JobTask, Scheduler, SchedulerBuilder}
 | 
					import docspell.joex.scheduler._
 | 
				
			||||||
import docspell.store.Store
 | 
					import docspell.store.Store
 | 
				
			||||||
 | 
					import docspell.store.queue._
 | 
				
			||||||
import docspell.store.ops.ONode
 | 
					import docspell.store.ops.ONode
 | 
				
			||||||
import docspell.store.records.RJobLog
 | 
					import docspell.store.records.RJobLog
 | 
				
			||||||
import fs2.concurrent.SignallingRef
 | 
					import fs2.concurrent.SignallingRef
 | 
				
			||||||
@@ -17,14 +19,18 @@ final class JoexAppImpl[F[_]: ConcurrentEffect: ContextShift: Timer](
 | 
				
			|||||||
    nodeOps: ONode[F],
 | 
					    nodeOps: ONode[F],
 | 
				
			||||||
    store: Store[F],
 | 
					    store: Store[F],
 | 
				
			||||||
    termSignal: SignallingRef[F, Boolean],
 | 
					    termSignal: SignallingRef[F, Boolean],
 | 
				
			||||||
    val scheduler: Scheduler[F]
 | 
					    val scheduler: Scheduler[F],
 | 
				
			||||||
 | 
					    val periodicScheduler: PeriodicScheduler[F]
 | 
				
			||||||
) extends JoexApp[F] {
 | 
					) extends JoexApp[F] {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def init: F[Unit] = {
 | 
					  def init: F[Unit] = {
 | 
				
			||||||
    val run = scheduler.start.compile.drain
 | 
					    val run  = scheduler.start.compile.drain
 | 
				
			||||||
 | 
					    val prun = periodicScheduler.start.compile.drain
 | 
				
			||||||
    for {
 | 
					    for {
 | 
				
			||||||
      _ <- ConcurrentEffect[F].start(run)
 | 
					      _ <- ConcurrentEffect[F].start(run)
 | 
				
			||||||
 | 
					      _ <- ConcurrentEffect[F].start(prun)
 | 
				
			||||||
      _ <- scheduler.periodicAwake
 | 
					      _ <- scheduler.periodicAwake
 | 
				
			||||||
 | 
					      _ <- periodicScheduler.periodicAwake
 | 
				
			||||||
      _ <- nodeOps.register(cfg.appId, NodeType.Joex, cfg.baseUrl)
 | 
					      _ <- nodeOps.register(cfg.appId, NodeType.Joex, cfg.baseUrl)
 | 
				
			||||||
    } yield ()
 | 
					    } yield ()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -36,7 +42,7 @@ final class JoexAppImpl[F[_]: ConcurrentEffect: ContextShift: Timer](
 | 
				
			|||||||
    nodeOps.unregister(cfg.appId)
 | 
					    nodeOps.unregister(cfg.appId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def initShutdown: F[Unit] =
 | 
					  def initShutdown: F[Unit] =
 | 
				
			||||||
    scheduler.shutdown(false) *> termSignal.set(true)
 | 
					    periodicScheduler.shutdown *> scheduler.shutdown(false) *> termSignal.set(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -50,8 +56,12 @@ object JoexAppImpl {
 | 
				
			|||||||
  ): Resource[F, JoexApp[F]] =
 | 
					  ): Resource[F, JoexApp[F]] =
 | 
				
			||||||
    for {
 | 
					    for {
 | 
				
			||||||
      store   <- Store.create(cfg.jdbc, connectEC, blocker)
 | 
					      store   <- Store.create(cfg.jdbc, connectEC, blocker)
 | 
				
			||||||
 | 
					      queue   <- JobQueue(store)
 | 
				
			||||||
 | 
					      pstore  <- PeriodicTaskStore.create(store)
 | 
				
			||||||
      nodeOps <- ONode(store)
 | 
					      nodeOps <- ONode(store)
 | 
				
			||||||
 | 
					      psch    <- PeriodicScheduler.create(cfg.periodicScheduler, queue, pstore, Timer[F])
 | 
				
			||||||
      sch <- SchedulerBuilder(cfg.scheduler, blocker, store)
 | 
					      sch <- SchedulerBuilder(cfg.scheduler, blocker, store)
 | 
				
			||||||
 | 
					        .withQueue(queue)
 | 
				
			||||||
        .withTask(
 | 
					        .withTask(
 | 
				
			||||||
          JobTask.json(
 | 
					          JobTask.json(
 | 
				
			||||||
            ProcessItemArgs.taskName,
 | 
					            ProcessItemArgs.taskName,
 | 
				
			||||||
@@ -59,8 +69,15 @@ object JoexAppImpl {
 | 
				
			|||||||
            ItemHandler.onCancel[F]
 | 
					            ItemHandler.onCancel[F]
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        .withTask(
 | 
				
			||||||
 | 
					          JobTask.json(
 | 
				
			||||||
 | 
					            PeriodicTask.taskName,
 | 
				
			||||||
 | 
					            PeriodicTask[F](cfg),
 | 
				
			||||||
 | 
					            PeriodicTask.onCancel[F]
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        .resource
 | 
					        .resource
 | 
				
			||||||
      app = new JoexAppImpl(cfg, nodeOps, store, termSignal, sch)
 | 
					      app = new JoexAppImpl(cfg, nodeOps, store, termSignal, sch, psch)
 | 
				
			||||||
      appR <- Resource.make(app.init.map(_ => app))(_.shutdown)
 | 
					      appR <- Resource.make(app.init.map(_ => app))(_.shutdown)
 | 
				
			||||||
    } yield appR
 | 
					    } yield appR
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,8 @@ object JoexRoutes {
 | 
				
			|||||||
      case POST -> Root / "notify" =>
 | 
					      case POST -> Root / "notify" =>
 | 
				
			||||||
        for {
 | 
					        for {
 | 
				
			||||||
          _    <- app.scheduler.notifyChange
 | 
					          _    <- app.scheduler.notifyChange
 | 
				
			||||||
          resp <- Ok(BasicResult(true, "Scheduler notified."))
 | 
					          _    <- app.periodicScheduler.notifyChange
 | 
				
			||||||
 | 
					          resp <- Ok(BasicResult(true, "Schedulers notified."))
 | 
				
			||||||
        } yield resp
 | 
					        } yield resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case GET -> Root / "running" =>
 | 
					      case GET -> Root / "running" =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package docspell.joex.scheduler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import fs2._
 | 
				
			||||||
 | 
					import fs2.concurrent.SignallingRef
 | 
				
			||||||
 | 
					import cats.effect._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import docspell.store.queue._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** A periodic scheduler takes care to submit periodic tasks to the
 | 
				
			||||||
 | 
					  * job queue.
 | 
				
			||||||
 | 
					  *
 | 
				
			||||||
 | 
					  * It is run in the background to regularily find a periodic task to
 | 
				
			||||||
 | 
					  * execute. If the task is due, it will be submitted into the job
 | 
				
			||||||
 | 
					  * queue where it will be picked up by the scheduler from some joex
 | 
				
			||||||
 | 
					  * instance. If it is due in the future, a notification is scheduled
 | 
				
			||||||
 | 
					  * to be received at that time so the task can be looked up again.
 | 
				
			||||||
 | 
					  */
 | 
				
			||||||
 | 
					trait PeriodicScheduler[F[_]] {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def config: PeriodicSchedulerConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def start: Stream[F, Nothing]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def shutdown: F[Unit]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def periodicAwake: F[Fiber[F, Unit]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def notifyChange: F[Unit]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object PeriodicScheduler {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create[F[_]: ConcurrentEffect: ContextShift](
 | 
				
			||||||
 | 
					      cfg: PeriodicSchedulerConfig,
 | 
				
			||||||
 | 
					      queue: JobQueue[F],
 | 
				
			||||||
 | 
					      store: PeriodicTaskStore[F],
 | 
				
			||||||
 | 
					      timer: Timer[F]
 | 
				
			||||||
 | 
					  ): Resource[F, PeriodicScheduler[F]] =
 | 
				
			||||||
 | 
					    for {
 | 
				
			||||||
 | 
					      waiter <- Resource.liftF(SignallingRef(true))
 | 
				
			||||||
 | 
					      state  <- Resource.liftF(SignallingRef(PeriodicSchedulerImpl.emptyState[F]))
 | 
				
			||||||
 | 
					      psch = new PeriodicSchedulerImpl[F](cfg, queue, store, waiter, state, timer)
 | 
				
			||||||
 | 
					      _ <- Resource.liftF(psch.init)
 | 
				
			||||||
 | 
					    } yield psch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					package docspell.joex.scheduler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import docspell.common._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					case class PeriodicSchedulerConfig(
 | 
				
			||||||
 | 
					  name: Ident,
 | 
				
			||||||
 | 
					  wakeupPeriod: Duration
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@@ -0,0 +1,194 @@
 | 
				
			|||||||
 | 
					package docspell.joex.scheduler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import fs2._
 | 
				
			||||||
 | 
					import fs2.concurrent.SignallingRef
 | 
				
			||||||
 | 
					import cats.effect._
 | 
				
			||||||
 | 
					import cats.implicits._
 | 
				
			||||||
 | 
					import org.log4s.getLogger
 | 
				
			||||||
 | 
					import com.github.eikek.fs2calev._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import docspell.common._
 | 
				
			||||||
 | 
					import docspell.common.syntax.all._
 | 
				
			||||||
 | 
					import docspell.store.queue._
 | 
				
			||||||
 | 
					import docspell.store.records.RPeriodicTask
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import PeriodicSchedulerImpl.State
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					onStartUp:
 | 
				
			||||||
 | 
					- remove worker value from all of the current
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Loop:
 | 
				
			||||||
 | 
					- get earliest pjob
 | 
				
			||||||
 | 
					- if none: stop
 | 
				
			||||||
 | 
					- if triggered:
 | 
				
			||||||
 | 
					  - mark worker, restart loop on fail
 | 
				
			||||||
 | 
					  - submit new job
 | 
				
			||||||
 | 
					    - check for non-final jobs of that name
 | 
				
			||||||
 | 
					    - if exist: log info
 | 
				
			||||||
 | 
					    - if not exist: submit
 | 
				
			||||||
 | 
					  - update next trigger (in both cases)
 | 
				
			||||||
 | 
					  - remove worker
 | 
				
			||||||
 | 
					  - restart loop
 | 
				
			||||||
 | 
					- if future
 | 
				
			||||||
 | 
					  - schedule notify
 | 
				
			||||||
 | 
					  - stop loop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onNotify:
 | 
				
			||||||
 | 
					- cancel current scheduled notify
 | 
				
			||||||
 | 
					- start Loop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onShutdown:
 | 
				
			||||||
 | 
					- nothing to do
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class PeriodicSchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
 | 
				
			||||||
 | 
					    val config: PeriodicSchedulerConfig,
 | 
				
			||||||
 | 
					    queue: JobQueue[F],
 | 
				
			||||||
 | 
					    store: PeriodicTaskStore[F],
 | 
				
			||||||
 | 
					    waiter: SignallingRef[F, Boolean],
 | 
				
			||||||
 | 
					    state: SignallingRef[F, State[F]],
 | 
				
			||||||
 | 
					    timer: Timer[F]
 | 
				
			||||||
 | 
					) extends PeriodicScheduler[F] {
 | 
				
			||||||
 | 
					  private[this] val logger              = getLogger
 | 
				
			||||||
 | 
					  implicit private val _timer: Timer[F] = timer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def start: Stream[F, Nothing] =
 | 
				
			||||||
 | 
					    logger.sinfo("Starting periodic scheduler") ++
 | 
				
			||||||
 | 
					      mainLoop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def shutdown: F[Unit] =
 | 
				
			||||||
 | 
					    state.modify(_.requestShutdown)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def periodicAwake: F[Fiber[F, Unit]] =
 | 
				
			||||||
 | 
					    ConcurrentEffect[F].start(
 | 
				
			||||||
 | 
					      Stream
 | 
				
			||||||
 | 
					        .awakeEvery[F](config.wakeupPeriod.toScala)
 | 
				
			||||||
 | 
					        .evalMap(_ => logger.fdebug("Periodic awake reached") *> notifyChange)
 | 
				
			||||||
 | 
					        .compile
 | 
				
			||||||
 | 
					        .drain
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def notifyChange: F[Unit] =
 | 
				
			||||||
 | 
					    waiter.update(b => !b)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					    * On startup, get all periodic jobs from this scheduler and remove
 | 
				
			||||||
 | 
					    * the mark, so they get picked up again.
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					  def init: F[Unit] =
 | 
				
			||||||
 | 
					    logError("Error clearing marks")(store.clearMarks(config.name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def mainLoop: Stream[F, Nothing] = {
 | 
				
			||||||
 | 
					    val body: F[Boolean] =
 | 
				
			||||||
 | 
					      for {
 | 
				
			||||||
 | 
					        _   <- logger.fdebug(s"Going into main loop")
 | 
				
			||||||
 | 
					        now <- Timestamp.current[F]
 | 
				
			||||||
 | 
					        _   <- logger.fdebug(s"Looking for next periodic task")
 | 
				
			||||||
 | 
					        go <- logThrow("Error getting next task")(
 | 
				
			||||||
 | 
					          store
 | 
				
			||||||
 | 
					            .takeNext(config.name)
 | 
				
			||||||
 | 
					            .use({
 | 
				
			||||||
 | 
					              case Some(pj) =>
 | 
				
			||||||
 | 
					                (if (isTriggered(pj, now)) submitJob(pj)
 | 
				
			||||||
 | 
					                 else scheduleNotify(pj)).map(_ => true)
 | 
				
			||||||
 | 
					              case None =>
 | 
				
			||||||
 | 
					                false.pure[F]
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      } yield go
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Stream
 | 
				
			||||||
 | 
					      .eval(state.get.map(_.shutdownRequest))
 | 
				
			||||||
 | 
					      .evalTap(
 | 
				
			||||||
 | 
					        if (_) logger.finfo[F]("Stopping main loop due to shutdown request.")
 | 
				
			||||||
 | 
					        else ().pure[F]
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .flatMap(if (_) Stream.empty else Stream.eval(cancelNotify *> body))
 | 
				
			||||||
 | 
					      .flatMap({
 | 
				
			||||||
 | 
					        case true =>
 | 
				
			||||||
 | 
					          mainLoop
 | 
				
			||||||
 | 
					        case false =>
 | 
				
			||||||
 | 
					          logger.sdebug(s"Waiting for notify") ++
 | 
				
			||||||
 | 
					            waiter.discrete.take(2).drain ++
 | 
				
			||||||
 | 
					            logger.sdebug(s"Notify signal, going into main loop") ++
 | 
				
			||||||
 | 
					            mainLoop
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def isTriggered(pj: RPeriodicTask, now: Timestamp): Boolean =
 | 
				
			||||||
 | 
					    pj.timer.contains(now.value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def submitJob(pj: RPeriodicTask): F[Unit] =
 | 
				
			||||||
 | 
					    store
 | 
				
			||||||
 | 
					      .findNonFinalJob(pj.id)
 | 
				
			||||||
 | 
					      .flatMap({
 | 
				
			||||||
 | 
					        case Some(job) =>
 | 
				
			||||||
 | 
					          logger.finfo[F](
 | 
				
			||||||
 | 
					            s"There is already a job with non-final state '${job.state}' in the queue"
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case None =>
 | 
				
			||||||
 | 
					          logger.finfo[F](s"Submitting job for periodic task '${pj.task.id}'") *>
 | 
				
			||||||
 | 
					            pj.toJob.flatMap(queue.insert)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def scheduleNotify(pj: RPeriodicTask): F[Unit] =
 | 
				
			||||||
 | 
					    ConcurrentEffect[F]
 | 
				
			||||||
 | 
					      .start(
 | 
				
			||||||
 | 
					        CalevFs2
 | 
				
			||||||
 | 
					          .sleep[F](pj.timer)
 | 
				
			||||||
 | 
					          .evalMap(_ => notifyChange)
 | 
				
			||||||
 | 
					          .compile
 | 
				
			||||||
 | 
					          .drain
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .flatMap(fb => state.modify(_.setNotify(fb)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def cancelNotify: F[Unit] =
 | 
				
			||||||
 | 
					    state
 | 
				
			||||||
 | 
					      .modify(_.clearNotify)
 | 
				
			||||||
 | 
					      .flatMap({
 | 
				
			||||||
 | 
					        case Some(fb) =>
 | 
				
			||||||
 | 
					          fb.cancel
 | 
				
			||||||
 | 
					        case None =>
 | 
				
			||||||
 | 
					          ().pure[F]
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def logError(msg: => String)(fa: F[Unit]): F[Unit] =
 | 
				
			||||||
 | 
					    fa.attempt.flatMap {
 | 
				
			||||||
 | 
					      case Right(_) => ().pure[F]
 | 
				
			||||||
 | 
					      case Left(ex) => logger.ferror(ex)(msg).map(_ => ())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def logThrow[A](msg: => String)(fa: F[A]): F[A] =
 | 
				
			||||||
 | 
					    fa.attempt
 | 
				
			||||||
 | 
					      .flatMap({
 | 
				
			||||||
 | 
					        case r @ Right(_) => (r: Either[Throwable, A]).pure[F]
 | 
				
			||||||
 | 
					        case l @ Left(ex) => logger.ferror(ex)(msg).map(_ => (l: Either[Throwable, A]))
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .rethrow
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object PeriodicSchedulerImpl {
 | 
				
			||||||
 | 
					  def emptyState[F[_]]: State[F] =
 | 
				
			||||||
 | 
					    State(false, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class State[F[_]](
 | 
				
			||||||
 | 
					      shutdownRequest: Boolean,
 | 
				
			||||||
 | 
					      scheduledNotify: Option[Fiber[F, Unit]]
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    def requestShutdown: (State[F], Unit) =
 | 
				
			||||||
 | 
					      (copy(shutdownRequest = true), ())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setNotify(fb: Fiber[F, Unit]): (State[F], Unit) =
 | 
				
			||||||
 | 
					      (copy(scheduledNotify = Some(fb)), ())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clearNotify: (State[F], Option[Fiber[F, Unit]]) =
 | 
				
			||||||
 | 
					      (copy(scheduledNotify = None), scheduledNotify)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -34,6 +34,9 @@ case class SchedulerBuilder[F[_]: ConcurrentEffect: ContextShift](
 | 
				
			|||||||
  def withLogSink(sink: LogSink[F]): SchedulerBuilder[F] =
 | 
					  def withLogSink(sink: LogSink[F]): SchedulerBuilder[F] =
 | 
				
			||||||
    copy(logSink = sink)
 | 
					    copy(logSink = sink)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def withQueue(queue: JobQueue[F]): SchedulerBuilder[F] =
 | 
				
			||||||
 | 
					    copy(queue = Resource.pure[F, JobQueue[F]](queue))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def serve: Resource[F, Scheduler[F]] =
 | 
					  def serve: Resource[F, Scheduler[F]] =
 | 
				
			||||||
    resource.evalMap(sch => ConcurrentEffect[F].start(sch.start.compile.drain).map(_ => sch))
 | 
					    resource.evalMap(sch => ConcurrentEffect[F].start(sch.start.compile.drain).map(_ => sch))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,7 +118,7 @@ trait Conversions {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def mkItemList(v: Vector[OItem.ListItem]): ItemLightList = {
 | 
					  def mkItemList(v: Vector[OItem.ListItem]): ItemLightList = {
 | 
				
			||||||
    val groups = v.groupBy(item => item.date.toDate.toString.substring(0, 7))
 | 
					    val groups = v.groupBy(item => item.date.toUtcDate.toString.substring(0, 7))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def mkGroup(g: (String, Vector[OItem.ListItem])): ItemLightGroup =
 | 
					    def mkGroup(g: (String, Vector[OItem.ListItem])): ItemLightGroup =
 | 
				
			||||||
      ItemLightGroup(g._1, g._2.map(mkItemLight).toList)
 | 
					      ItemLightGroup(g._1, g._2.map(mkItemLight).toList)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					CREATE TABLE `periodic_task` (
 | 
				
			||||||
 | 
					  `id` varchar(254) not null primary key,
 | 
				
			||||||
 | 
					  `enabled` boolean not null,
 | 
				
			||||||
 | 
					  `task` varchar(254) not null,
 | 
				
			||||||
 | 
					  `group_` varchar(254) not null,
 | 
				
			||||||
 | 
					  `args` text not null,
 | 
				
			||||||
 | 
					  `subject` varchar(254) not null,
 | 
				
			||||||
 | 
					  `submitter` varchar(254) not null,
 | 
				
			||||||
 | 
					  `priority` int not null,
 | 
				
			||||||
 | 
					  `worker` varchar(254),
 | 
				
			||||||
 | 
					  `marked` timestamp,
 | 
				
			||||||
 | 
					  `timer` varchar(254) not null,
 | 
				
			||||||
 | 
					  `nextrun` timestamp not null,
 | 
				
			||||||
 | 
					  `created` timestamp not null
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					CREATE TABLE "periodic_task" (
 | 
				
			||||||
 | 
					  "id" varchar(254) not null primary key,
 | 
				
			||||||
 | 
					  "enabled" boolean not null,
 | 
				
			||||||
 | 
					  "task" varchar(254) not null,
 | 
				
			||||||
 | 
					  "group_" varchar(254) not null,
 | 
				
			||||||
 | 
					  "args" text not null,
 | 
				
			||||||
 | 
					  "subject" varchar(254) not null,
 | 
				
			||||||
 | 
					  "submitter" varchar(254) not null,
 | 
				
			||||||
 | 
					  "priority" int not null,
 | 
				
			||||||
 | 
					  "worker" varchar(254),
 | 
				
			||||||
 | 
					  "timer" varchar(254) not null,
 | 
				
			||||||
 | 
					  "nextrun" timestamp not null
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -7,6 +7,7 @@ import doobie._
 | 
				
			|||||||
import doobie.implicits.legacy.instant._
 | 
					import doobie.implicits.legacy.instant._
 | 
				
			||||||
import doobie.util.log.Success
 | 
					import doobie.util.log.Success
 | 
				
			||||||
import emil.{MailAddress, SSLType}
 | 
					import emil.{MailAddress, SSLType}
 | 
				
			||||||
 | 
					import com.github.eikek.calev.CalEvent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import docspell.common._
 | 
					import docspell.common._
 | 
				
			||||||
import docspell.common.syntax.all._
 | 
					import docspell.common.syntax.all._
 | 
				
			||||||
@@ -98,6 +99,9 @@ trait DoobieMeta {
 | 
				
			|||||||
    Meta[String].imap(str => str.split(',').toList.map(_.trim).map(EmilUtil.unsafeReadMailAddress))(
 | 
					    Meta[String].imap(str => str.split(',').toList.map(_.trim).map(EmilUtil.unsafeReadMailAddress))(
 | 
				
			||||||
      lma => lma.map(EmilUtil.mailAddressString).mkString(",")
 | 
					      lma => lma.map(EmilUtil.mailAddressString).mkString(",")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  implicit val metaCalEvent: Meta[CalEvent] =
 | 
				
			||||||
 | 
					    Meta[String].timap(CalEvent.unsafe)(_.asString)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object DoobieMeta extends DoobieMeta {
 | 
					object DoobieMeta extends DoobieMeta {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					package docspell.store.queue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cats.effect._
 | 
				
			||||||
 | 
					import cats.implicits._
 | 
				
			||||||
 | 
					import fs2.Stream
 | 
				
			||||||
 | 
					import docspell.common._
 | 
				
			||||||
 | 
					import docspell.store.Store
 | 
				
			||||||
 | 
					import docspell.store.records._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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): Resource[F, Option[RPeriodicTask]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def clearMarks(name: Ident): F[Unit]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def findNonFinalJob(pjobId: Ident): F[Option[RJob]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def insert(task: RPeriodicTask): F[Unit]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object PeriodicTaskStore {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create[F[_]: Sync](store: Store[F]): Resource[F, PeriodicTaskStore[F]] =
 | 
				
			||||||
 | 
					    Resource.pure[F, PeriodicTaskStore[F]](new PeriodicTaskStore[F] {
 | 
				
			||||||
 | 
					      println(s"$store")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      def takeNext(worker: Ident): Resource[F, Option[RPeriodicTask]] = {
 | 
				
			||||||
 | 
					        val chooseNext: F[Either[String, Option[RPeriodicTask]]] =
 | 
				
			||||||
 | 
					          getNext.flatMap {
 | 
				
			||||||
 | 
					            case Some(pj) =>
 | 
				
			||||||
 | 
					              mark(pj.id, worker).map {
 | 
				
			||||||
 | 
					                case true  => Right(Some(pj.copy(worker = worker.some)))
 | 
				
			||||||
 | 
					                case false => Left("Cannot mark periodic task")
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            case None =>
 | 
				
			||||||
 | 
					              val result: Either[String, Option[RPeriodicTask]] =
 | 
				
			||||||
 | 
					                Right(None)
 | 
				
			||||||
 | 
					              result.pure[F]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        val get =
 | 
				
			||||||
 | 
					          Stream.eval(chooseNext).repeat.take(10).find(_.isRight).compile.lastOrError
 | 
				
			||||||
 | 
					        val r = Resource.make(get)({
 | 
				
			||||||
 | 
					          case Right(Some(pj)) => unmark(pj)
 | 
				
			||||||
 | 
					          case _               => ().pure[F]
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        r.flatMap {
 | 
				
			||||||
 | 
					          case Right(job) => Resource.pure(job)
 | 
				
			||||||
 | 
					          case Left(err)  => Resource.liftF(Sync[F].raiseError(new Exception(err)))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      def getNext: F[Option[RPeriodicTask]] =
 | 
				
			||||||
 | 
					        Sync[F].raiseError(new Exception("not implemented"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      def mark(pid: Ident, name: Ident): F[Boolean] =
 | 
				
			||||||
 | 
					        Sync[F].raiseError(new Exception(s"not implemented $pid $name"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      def unmark(job: RPeriodicTask): F[Unit] =
 | 
				
			||||||
 | 
					        Sync[F].raiseError(new Exception(s"not implemented $job"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      def clearMarks(name: Ident): F[Unit] =
 | 
				
			||||||
 | 
					        Sync[F].raiseError(new Exception("not implemented"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      def findNonFinalJob(pjobId: Ident): F[Option[RJob]] =
 | 
				
			||||||
 | 
					        Sync[F].raiseError(new Exception("not implemented"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      def insert(task: RPeriodicTask): F[Unit] =
 | 
				
			||||||
 | 
					        Sync[F].raiseError(new Exception("not implemented"))
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,141 @@
 | 
				
			|||||||
 | 
					package docspell.store.records
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cats.effect._
 | 
				
			||||||
 | 
					import cats.implicits._
 | 
				
			||||||
 | 
					import doobie._
 | 
				
			||||||
 | 
					import doobie.implicits._
 | 
				
			||||||
 | 
					import com.github.eikek.calev.CalEvent
 | 
				
			||||||
 | 
					import docspell.common._
 | 
				
			||||||
 | 
					import docspell.store.impl.Column
 | 
				
			||||||
 | 
					import docspell.store.impl.Implicits._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** A periodic task is a special job description, that shares a few
 | 
				
			||||||
 | 
					  * properties of a `RJob`. It must provide all information to create
 | 
				
			||||||
 | 
					  * a `RJob` value eventually.
 | 
				
			||||||
 | 
					  */
 | 
				
			||||||
 | 
					case class RPeriodicTask(
 | 
				
			||||||
 | 
					    id: Ident,
 | 
				
			||||||
 | 
					    enabled: Boolean,
 | 
				
			||||||
 | 
					    task: Ident,
 | 
				
			||||||
 | 
					    group: Ident,
 | 
				
			||||||
 | 
					    args: String,
 | 
				
			||||||
 | 
					    subject: String,
 | 
				
			||||||
 | 
					    submitter: Ident,
 | 
				
			||||||
 | 
					    priority: Priority,
 | 
				
			||||||
 | 
					    worker: Option[Ident],
 | 
				
			||||||
 | 
					    marked: Option[Timestamp],
 | 
				
			||||||
 | 
					    timer: CalEvent,
 | 
				
			||||||
 | 
					    nextrun: Timestamp,
 | 
				
			||||||
 | 
					    created: Timestamp
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def toJob[F[_]: Sync]: F[RJob] =
 | 
				
			||||||
 | 
					    for {
 | 
				
			||||||
 | 
					      now <- Timestamp.current[F]
 | 
				
			||||||
 | 
					      jid <- Ident.randomId[F]
 | 
				
			||||||
 | 
					    } yield RJob(
 | 
				
			||||||
 | 
					      jid,
 | 
				
			||||||
 | 
					      task,
 | 
				
			||||||
 | 
					      group,
 | 
				
			||||||
 | 
					      args,
 | 
				
			||||||
 | 
					      subject,
 | 
				
			||||||
 | 
					      now,
 | 
				
			||||||
 | 
					      submitter,
 | 
				
			||||||
 | 
					      priority,
 | 
				
			||||||
 | 
					      JobState.Waiting,
 | 
				
			||||||
 | 
					      0,
 | 
				
			||||||
 | 
					      0,
 | 
				
			||||||
 | 
					      Some(id),
 | 
				
			||||||
 | 
					      None,
 | 
				
			||||||
 | 
					      None,
 | 
				
			||||||
 | 
					      None
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object RPeriodicTask {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create[F[_]: Sync](
 | 
				
			||||||
 | 
					      enabled: Boolean,
 | 
				
			||||||
 | 
					      task: Ident,
 | 
				
			||||||
 | 
					      group: Ident,
 | 
				
			||||||
 | 
					      args: String,
 | 
				
			||||||
 | 
					      subject: String,
 | 
				
			||||||
 | 
					      submitter: Ident,
 | 
				
			||||||
 | 
					      priority: Priority,
 | 
				
			||||||
 | 
					      worker: Option[Ident],
 | 
				
			||||||
 | 
					      marked: Option[Timestamp],
 | 
				
			||||||
 | 
					      timer: CalEvent
 | 
				
			||||||
 | 
					  ): F[RPeriodicTask] =
 | 
				
			||||||
 | 
					    Ident
 | 
				
			||||||
 | 
					      .randomId[F]
 | 
				
			||||||
 | 
					      .flatMap(id =>
 | 
				
			||||||
 | 
					        Timestamp
 | 
				
			||||||
 | 
					          .current[F]
 | 
				
			||||||
 | 
					          .map { now =>
 | 
				
			||||||
 | 
					            RPeriodicTask(
 | 
				
			||||||
 | 
					              id,
 | 
				
			||||||
 | 
					              enabled,
 | 
				
			||||||
 | 
					              task,
 | 
				
			||||||
 | 
					              group,
 | 
				
			||||||
 | 
					              args,
 | 
				
			||||||
 | 
					              subject,
 | 
				
			||||||
 | 
					              submitter,
 | 
				
			||||||
 | 
					              priority,
 | 
				
			||||||
 | 
					              worker,
 | 
				
			||||||
 | 
					              marked,
 | 
				
			||||||
 | 
					              timer,
 | 
				
			||||||
 | 
					              timer
 | 
				
			||||||
 | 
					                .nextElapse(now.atZone(Timestamp.UTC))
 | 
				
			||||||
 | 
					                .map(_.toInstant)
 | 
				
			||||||
 | 
					                .map(Timestamp.apply)
 | 
				
			||||||
 | 
					                .getOrElse(Timestamp.Epoch),
 | 
				
			||||||
 | 
					              now
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val table = fr"periodic_task"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  object Columns {
 | 
				
			||||||
 | 
					    val id        = Column("id")
 | 
				
			||||||
 | 
					    val enabled   = Column("enabled")
 | 
				
			||||||
 | 
					    val task      = Column("task")
 | 
				
			||||||
 | 
					    val group     = Column("group_")
 | 
				
			||||||
 | 
					    val args      = Column("args")
 | 
				
			||||||
 | 
					    val subject   = Column("subject")
 | 
				
			||||||
 | 
					    val submitter = Column("submitter")
 | 
				
			||||||
 | 
					    val priority  = Column("priority")
 | 
				
			||||||
 | 
					    val worker    = Column("worker")
 | 
				
			||||||
 | 
					    val marked    = Column("marked")
 | 
				
			||||||
 | 
					    val timer     = Column("timer")
 | 
				
			||||||
 | 
					    val nextrun   = Column("nextrun")
 | 
				
			||||||
 | 
					    val created   = Column("created")
 | 
				
			||||||
 | 
					    val all = List(
 | 
				
			||||||
 | 
					      id,
 | 
				
			||||||
 | 
					      enabled,
 | 
				
			||||||
 | 
					      task,
 | 
				
			||||||
 | 
					      group,
 | 
				
			||||||
 | 
					      args,
 | 
				
			||||||
 | 
					      subject,
 | 
				
			||||||
 | 
					      submitter,
 | 
				
			||||||
 | 
					      priority,
 | 
				
			||||||
 | 
					      worker,
 | 
				
			||||||
 | 
					      marked,
 | 
				
			||||||
 | 
					      timer,
 | 
				
			||||||
 | 
					      nextrun,
 | 
				
			||||||
 | 
					      created
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  import Columns._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def insert(v: RPeriodicTask): ConnectionIO[Int] = {
 | 
				
			||||||
 | 
					    val sql = insertRow(
 | 
				
			||||||
 | 
					      table,
 | 
				
			||||||
 | 
					      all,
 | 
				
			||||||
 | 
					      fr"${v.id},${v.enabled},${v.task},${v.group},${v.args},${v.subject},${v.submitter},${v.priority},${v.worker},${v.marked},${v.timer},${v.nextrun},${v.created}"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    sql.update.run
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -7,6 +7,7 @@ object Dependencies {
 | 
				
			|||||||
  val BcryptVersion = "0.4"
 | 
					  val BcryptVersion = "0.4"
 | 
				
			||||||
  val BetterMonadicForVersion = "0.3.1"
 | 
					  val BetterMonadicForVersion = "0.3.1"
 | 
				
			||||||
  val BitpeaceVersion = "0.4.3"
 | 
					  val BitpeaceVersion = "0.4.3"
 | 
				
			||||||
 | 
					  val CalevVersion = "0.1.0"
 | 
				
			||||||
  val CirceVersion = "0.13.0"
 | 
					  val CirceVersion = "0.13.0"
 | 
				
			||||||
  val DoobieVersion = "0.8.8"
 | 
					  val DoobieVersion = "0.8.8"
 | 
				
			||||||
  val EmilVersion = "0.2.0"
 | 
					  val EmilVersion = "0.2.0"
 | 
				
			||||||
@@ -37,6 +38,11 @@ object Dependencies {
 | 
				
			|||||||
  val ViewerJSVersion = "0.5.8"
 | 
					  val ViewerJSVersion = "0.5.8"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val calev = Seq(
 | 
				
			||||||
 | 
					    "com.github.eikek" %% "calev-core" % CalevVersion,
 | 
				
			||||||
 | 
					    "com.github.eikek" %% "calev-fs2" % CalevVersion
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val jclOverSlf4j = Seq(
 | 
					  val jclOverSlf4j = Seq(
 | 
				
			||||||
    "org.slf4j" % "jcl-over-slf4j" % Slf4jVersion
 | 
					    "org.slf4j" % "jcl-over-slf4j" % Slf4jVersion
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user