{"id":24407822,"url":"https://github.com/itv/quartz4s","last_synced_at":"2025-04-12T01:16:54.796Z","repository":{"id":39802754,"uuid":"265602915","full_name":"ITV/quartz4s","owner":"ITV","description":"Quarts scheduler library based on cats-effect","archived":false,"fork":false,"pushed_at":"2022-11-23T09:10:25.000Z","size":279,"stargazers_count":5,"open_issues_count":2,"forks_count":2,"subscribers_count":31,"default_branch":"main","last_synced_at":"2025-04-12T01:16:49.343Z","etag":null,"topics":["streaming-library"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ITV.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-20T15:10:01.000Z","updated_at":"2024-02-29T09:55:30.000Z","dependencies_parsed_at":"2023-01-21T04:01:35.771Z","dependency_job_id":null,"html_url":"https://github.com/ITV/quartz4s","commit_stats":null,"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ITV%2Fquartz4s","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ITV%2Fquartz4s/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ITV%2Fquartz4s/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ITV%2Fquartz4s/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ITV","download_url":"https://codeload.github.com/ITV/quartz4s/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248501859,"owners_count":21114684,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["streaming-library"],"created_at":"2025-01-20T05:18:08.632Z","updated_at":"2025-04-12T01:16:54.766Z","avatar_url":"https://github.com/ITV.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# quartz4s\nQuarts scheduler library using cats-effect queues for handling results.\n\n### Import\n```scala\nlibraryDependencies ++= Seq(\n  \"com.itv\" %% \"quartz4s-core\"     % \"1.0.5+12-d29e6643-SNAPSHOT\",\n)\n```\n\nThe project uses a quartz scheduler, and as scheduled messages are generated from Quartz they are\ndecoded and put onto an `cats.effect.std.Queue`.\n\n#### Components for scheduling jobs:\n* a `QuartzTaskScheduler[F[_], A]` which schedules jobs of type `A`\n* a `JobDataEncoder[A]` which encodes job data in a map for the given job of type `A`\n\n#### Components for responding to scheduled messages:\n* a job factory which is triggered by quartz when a scheduled task occurs and creates messages to put on the queue\n* a `JobDecoder[A]` which decodes the incoming message data map into an `A`\n* the decoded message is put onto the provided `cats.effect.std.Queue`\n\n\n## Usage:\n\n## Create some job types\nWe need to have a set of types to encode and decode.\nWe provide the ability to encode/decode an object as a `Map[String, String]`, which works perfectly for \nputting data into the quartz `JobDataMap`. (Heavily inspired by [extruder](https://janstenpickle.github.io/extruder/)).\n```scala\nimport com.itv.scheduler.{JobDataEncoder, JobDecoder}\nimport com.itv.scheduler.extruder.semiauto._\n\nsealed trait ParentJob\ncase object ChildObjectJob     extends ParentJob\ncase class UserJob(id: String) extends ParentJob\n\nobject ParentJob {\n  implicit val jobDataEncoder: JobDataEncoder[ParentJob] = deriveJobEncoder[ParentJob]\n  implicit val jobDecoder: JobDecoder[ParentJob]         = deriveJobDecoder[ParentJob]\n  //or, simply: implicit val jobCodec: JobCodec[ParentJob] = deriveJobCodec[ParentJob]\n}\n```\n\n### Create a JobFactory\nThere are 2 options when creating a `CallbackJobFactory`: auto-acked and manually acked messages.\n\n#### Auto-Acked messages\nScheduled jobs from quartz are immediately acked and the resulting message of type `A` is placed on a `Queue[F, A]`.\nIf the message taken from the queue isn't handled cleanly then the resulting quartz job won't be re-run,\nas it has already been marked as successful. \n```scala\nimport cats.effect._\nimport com.itv.scheduler._\nimport cats.effect.std.Queue\nimport cats.effect.unsafe.implicits.global\n\nval jobMessageQueue = Queue.unbounded[IO, ParentJob].unsafeRunSync()\n// jobMessageQueue: Queue[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], ParentJob] = cats.effect.std.Queue$BoundedQueue@461ee05a\nval autoAckJobFactory = MessageQueueJobFactory.autoAcking[IO, ParentJob](jobMessageQueue)\n// autoAckJobFactory: Resource[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], AutoAckingQueueJobFactory[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], ParentJob]] = Bind(\n//   source = Bind(\n//     source = Bind(\n//       source = Allocate(\n//         resource = cats.effect.kernel.Resource$$$Lambda$15134/0x0000000804148840@7bb872ea\n//       ),\n//       fs = cats.effect.kernel.Resource$$Lambda$15136/0x000000080414a040@4c3acfdb\n//     ),\n//     fs = cats.effect.std.Dispatcher$$$Lambda$15137/0x000000080414b040@89f3d7e\n//   ),\n//   fs = cats.effect.kernel.Resource$$Lambda$15136/0x000000080414a040@5de0523b\n// )\n```\n\n#### Manually Acked messages\nScheduled jobs are received but only acked with quartz once the handler has completed via an `acker: MessageAcker[F, A]`.\n\nScheduled jobs from quartz are bundled into a `message: A` and an `acker: MessageAcker[F, A]`.\nThe items in the queue are each a `Resource[F, A]` which uses the message and acks the message as the `Resource` is `use`d.\n\nAlternatively the lower-level way of handling each message is via a queue of\n`AckableMessage[F, A](message: A, acker: MessageAcker[F, A])` items where the message is explicitly acked by the user.\n\nIn both cases, the quartz job is only marked as complete once the `acker.complete(result: Either[Throwable, Unit])` is called.\n```scala\n// each message is wrapped as a `Resource` which acks on completion\nval ackableJobResourceMessageQueue = Queue.unbounded[IO, Resource[IO, ParentJob]].unsafeRunSync()\n// ackableJobResourceMessageQueue: Queue[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], Resource[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], ParentJob]] = cats.effect.std.Queue$BoundedQueue@12b71557\nval ackingResourceJobFactory: Resource[IO, AckingQueueJobFactory[IO, Resource, ParentJob]] =\n  MessageQueueJobFactory.ackingResource(ackableJobResourceMessageQueue)\n// ackingResourceJobFactory: Resource[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], AckingQueueJobFactory[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], Resource, ParentJob]] = Bind(\n//   source = Bind(\n//     source = Bind(\n//       source = Allocate(\n//         resource = cats.effect.kernel.Resource$$$Lambda$15134/0x0000000804148840@86954cd\n//       ),\n//       fs = cats.effect.kernel.Resource$$Lambda$15136/0x000000080414a040@796cec73\n//     ),\n//     fs = cats.effect.std.Dispatcher$$$Lambda$15137/0x000000080414b040@264218ef\n//   ),\n//   fs = cats.effect.kernel.Resource$$Lambda$15136/0x000000080414a040@c75acdd\n// )\n\n// each message is wrapped as a `AckableMessage` which acks on completion\nval ackableJobMessageQueue = Queue.unbounded[IO, AckableMessage[IO, ParentJob]].unsafeRunSync()\n// ackableJobMessageQueue: Queue[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], AckableMessage[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], ParentJob]] = cats.effect.std.Queue$BoundedQueue@4ef85ea2\nval ackingJobFactory: Resource[IO, AckingQueueJobFactory[IO, AckableMessage, ParentJob]] =\n  MessageQueueJobFactory.acking(ackableJobMessageQueue)\n// ackingJobFactory: Resource[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], AckingQueueJobFactory[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], [F \u003e: Nothing \u003c: [_$3 \u003e: Nothing \u003c: Any] =\u003e Any, A \u003e: Nothing \u003c: Any] =\u003e AckableMessage[F, A], ParentJob]] = Bind(\n//   source = Bind(\n//     source = Bind(\n//       source = Allocate(\n//         resource = cats.effect.kernel.Resource$$$Lambda$15134/0x0000000804148840@50e92c7a\n//       ),\n//       fs = cats.effect.kernel.Resource$$Lambda$15136/0x000000080414a040@665ac3e8\n//     ),\n//     fs = cats.effect.std.Dispatcher$$$Lambda$15137/0x000000080414b040@47e898ba\n//   ),\n//   fs = cats.effect.kernel.Resource$$Lambda$15136/0x000000080414a040@4b69c3ab\n// )\n```\n\n### Creating a scheduler\n```scala\nval quartzProperties = QuartzProperties(new java.util.Properties())\n// quartzProperties: QuartzProperties = QuartzProperties(properties = {})\nval schedulerResource: Resource[IO, QuartzTaskScheduler[IO, ParentJob]] =\n  autoAckJobFactory.flatMap { jobFactory =\u003e \n    QuartzTaskScheduler[IO, ParentJob](quartzProperties, jobFactory)\n  }\n// schedulerResource: Resource[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], QuartzTaskScheduler[[A \u003e: Nothing \u003c: Any] =\u003e IO[A], ParentJob]] = Bind(\n//   source = Bind(\n//     source = Bind(\n//       source = Bind(\n//         source = Allocate(\n//           resource = cats.effect.kernel.Resource$$$Lambda$15134/0x0000000804148840@7bb872ea\n//         ),\n//         fs = cats.effect.kernel.Resource$$Lambda$15136/0x000000080414a040@4c3acfdb\n//       ),\n//       fs = cats.effect.std.Dispatcher$$$Lambda$15137/0x000000080414b040@89f3d7e\n//     ),\n//     fs = cats.effect.kernel.Resource$$Lambda$15136/0x000000080414a040@5de0523b\n//   ),\n//   fs = repl.MdocSession$MdocApp$$Lambda$15141/0x000000080414e840@43998d42\n// )\n```\n\n### Using the scheduler\n```scala\nimport java.time.Instant\nimport org.quartz.{CronExpression, JobKey, TriggerKey}\n\ndef scheduleCronJob(scheduler: QuartzTaskScheduler[IO, ParentJob]): IO[Option[Instant]] =\n  scheduler.scheduleJob(\n    JobKey.jobKey(\"child-object-job\"),\n    ChildObjectJob,\n    TriggerKey.triggerKey(\"cron-test-trigger\"),\n    CronScheduledJob(new CronExpression(\"* * * ? * *\"))\n  )\n\ndef scheduleSingleJob(scheduler: QuartzTaskScheduler[IO, ParentJob]): IO[Option[Instant]] =\n  scheduler.scheduleJob(\n    JobKey.jobKey(\"single-user-job\"),\n    UserJob(\"user-123\"),\n    TriggerKey.triggerKey(\"scheduled-single-test-trigger\"),\n    JobScheduledAt(Instant.now.plusSeconds(2))\n  )\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fitv%2Fquartz4s","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fitv%2Fquartz4s","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fitv%2Fquartz4s/lists"}