├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── freskog │ └── concurrency │ ├── app │ └── PartitioningDemo.scala │ └── partition │ ├── Common.scala │ ├── Partition.scala │ └── package.scala └── test └── scala └── freskog └── concurrency └── partition ├── BaseTest.scala └── ZIOBaseSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/sbt,scala,intellij+all 3 | # Edit at https://www.gitignore.io/?templates=sbt,scala,intellij+all 4 | 5 | ### Intellij+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # VSCode 33 | .bloop 34 | .metals 35 | project/.bloop 36 | 37 | # Gradle and Maven with auto-import 38 | # When using Gradle or Maven with auto-import, you should exclude module files, 39 | # since they will be recreated, and may cause churn. Uncomment if using 40 | # auto-import. 41 | # .idea/modules.xml 42 | # .idea/*.iml 43 | # .idea/modules 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | # JetBrains templates 79 | **___jb_tmp___ 80 | 81 | ### Intellij+all Patch ### 82 | # Ignores the whole .idea folder and all .iml files 83 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 84 | 85 | .idea/ 86 | 87 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 88 | 89 | *.iml 90 | modules.xml 91 | .idea/misc.xml 92 | *.ipr 93 | 94 | # Sonarlint plugin 95 | .idea/sonarlint 96 | 97 | ### SBT ### 98 | # Simple Build Tool 99 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 100 | 101 | dist/* 102 | target/ 103 | lib_managed/ 104 | src_managed/ 105 | project/boot/ 106 | project/plugins/project/ 107 | .history 108 | .cache 109 | .lib/ 110 | 111 | ### Scala ### 112 | *.class 113 | *.log 114 | 115 | # End of https://www.gitignore.io/api/sbt,scala,intellij+all 116 | 117 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.1.0" 2 | maxColumn = 120 3 | align = most 4 | continuationIndent.defnSite = 2 5 | assumeStandardLibraryStripMargin = true 6 | docstrings = JavaDoc 7 | lineEndings = preserve 8 | includeCurlyBraceInSelectChains = false 9 | danglingParentheses = true 10 | spaces { 11 | inImportCurlyBraces = true 12 | } 13 | optIn.annotationNewlines = true 14 | 15 | rewrite.rules = [SortImports, RedundantBraces] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stm-partitioning 2 | All the code from the blog post about exploring STM in ZIO. 3 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "stm-partitioning" 2 | 3 | version := "0.0.1" 4 | scalaVersion := "2.12.10" 5 | scalacOptions += ("-deprecation") 6 | val zioVersion = "1.0.0-RC16" 7 | val scalaTestVersion = "3.0.8" 8 | 9 | libraryDependencies ++= Seq( 10 | "dev.zio" %% "zio" % zioVersion, 11 | "dev.zio" %% "zio-test" % zioVersion % "test", 12 | "dev.zio" %% "zio-test-sbt" % zioVersion % "test" 13 | ) 14 | testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) 15 | 16 | addCommandAlias("com", "all compile test:compile") 17 | addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") 18 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.3 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.2.1") 2 | -------------------------------------------------------------------------------- /src/main/scala/freskog/concurrency/app/PartitioningDemo.scala: -------------------------------------------------------------------------------- 1 | package freskog.concurrency.app 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import zio.blocking.Blocking 6 | import zio.duration.Duration 7 | import zio.random.Random 8 | import zio.system.System 9 | import zio._ 10 | import zio.clock._ 11 | import zio.console._ 12 | 13 | import scala.concurrent.duration._ 14 | import freskog.concurrency.partition._ 15 | 16 | object PartitioningDemo extends App { 17 | type Environment = Console with Clock with Blocking with Random with System 18 | 19 | val config: Config = Config(userTTL = Duration(3, SECONDS), idleTTL = Duration(2, SECONDS), maxPending = 3) 20 | 21 | def brokenUserFunction(startTs: Long, counts: Ref[Map[Int, Int]])(n: Int): ZIO[Console with Clock, Nothing, Unit] = 22 | ZIO.descriptorWith( 23 | desc => 24 | for { 25 | now <- sleep(Duration(100 * n, MILLISECONDS)) *> currentTime(MILLISECONDS) 26 | m <- counts.update(m => m.updated(n, m.getOrElse(n, 0) + 1)) 27 | msg = s"Offset: ${now - startTs}ms Fiber: ${desc.id}, n = $n (call #${m(n)})" 28 | _ <- if (n == 0) throw new IllegalArgumentException(msg) else putStrLn(msg) 29 | } yield () 30 | ) 31 | 32 | val workItems: List[Int] = List.range(0, 11) ::: List.range(0, 11) ::: List.range(0, 11) ::: List(30) 33 | 34 | val program: ZIO[Environment with Partition, Nothing, Int] = 35 | for { 36 | now <- clock.currentTime(TimeUnit.MILLISECONDS) 37 | counter <- Ref.make(Map.empty[Int, Int]) 38 | env <- ZIO.environment[Console with Clock] 39 | process <- partition[Int](config, _.toString, brokenUserFunction(now, counter)(_).provide(env)) 40 | results <- ZIO.foreach(workItems)(process) 41 | _ <- console.putStrLn(s"Published ${results.count(identity)} out of ${results.length}") 42 | _ <- ZIO.sleep(Duration.fromScala(10.seconds)) 43 | } yield 0 44 | 45 | override def run(args: List[String]): ZIO[Environment, Nothing, Int] = 46 | program.provideSome[Environment]( 47 | env => 48 | new Clock with Console with System with Random with Blocking with Partition.Live { 49 | override val blocking: Blocking.Service[Any] = env.blocking 50 | override val clock: Clock.Service[Any] = env.clock 51 | override val console: Console.Service[Any] = env.console 52 | override val random: Random.Service[Any] = env.random 53 | override val system: System.Service[Any] = env.system 54 | } 55 | ) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/freskog/concurrency/partition/Common.scala: -------------------------------------------------------------------------------- 1 | package freskog.concurrency.partition 2 | 3 | import zio.clock.{ Clock } 4 | import zio.console.{ Console } 5 | import zio.duration.Duration 6 | import zio.stm.{ TQueue, TRef } 7 | 8 | trait Conf { 9 | def userTTL: Duration 10 | def idleTTL: Duration 11 | def maxPending: Int 12 | } 13 | 14 | case class Config(userTTL: Duration, idleTTL: Duration, maxPending: Int) 15 | 16 | package object Common { 17 | 18 | type PartEnv = Clock with Console with Conf 19 | type Queues[A] = TRef[Map[PartId, TQueue[A]]] 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/freskog/concurrency/partition/Partition.scala: -------------------------------------------------------------------------------- 1 | package freskog.concurrency.partition 2 | 3 | import zio.{ UIO, ZIO } 4 | import zio.clock.{ Clock } 5 | import zio.console.{ putStrLn, Console } 6 | import zio.duration.Duration 7 | import zio.stm.{ STM, TQueue, TRef } 8 | 9 | import freskog.concurrency.partition.Common._ 10 | 11 | trait Partition extends Serializable { 12 | val partition: Partition.Service[Any] 13 | } 14 | 15 | object Partition extends Serializable { 16 | 17 | trait Service[R] extends Serializable { 18 | def partition[A](config: Config, partIdOf: A => PartId, action: A => UIO[Unit]): ZIO[R, Nothing, A => UIO[Boolean]] 19 | } 20 | 21 | trait Live extends Clock.Live with Console.Live with Partition { 22 | env => 23 | 24 | override val partition: Service[Any] = 25 | new Service[Any] { 26 | override def partition[A]( 27 | config: Config, 28 | partIdOf: A => PartId, 29 | action: A => UIO[Unit] 30 | ): ZIO[Any, Nothing, A => UIO[Boolean]] = 31 | TRef 32 | .make(Map.empty[PartId, TQueue[A]]) 33 | .commit 34 | .map( 35 | queues => producer(queues, partIdOf, action)(_).provide(buildEnv(config, env)) 36 | ) 37 | } 38 | } 39 | 40 | def buildEnv(conf: Config, env: Clock with Console): PartEnv = 41 | new Conf with Clock with Console { 42 | override def userTTL: Duration = conf.userTTL 43 | override def idleTTL: Duration = conf.idleTTL 44 | override def maxPending: Int = conf.maxPending 45 | 46 | override val clock: Clock.Service[Any] = env.clock 47 | override val console: Console.Service[Any] = env.console 48 | } 49 | 50 | val userTTL: ZIO[Conf, Nothing, Duration] = 51 | ZIO.access[Conf](_.userTTL) 52 | 53 | val idleTTL: ZIO[Conf, Nothing, Duration] = 54 | ZIO.access[Conf](_.idleTTL) 55 | 56 | val maxPending: ZIO[Conf, Nothing, Int] = 57 | ZIO.access[Conf](_.maxPending) 58 | 59 | def publish[A](queue: TQueue[A], a: A): STM[Nothing, Boolean] = 60 | queue.size.flatMap(size => if (size == queue.capacity) STM.succeed(false) else queue.offer(a) *> STM.succeed(true)) 61 | 62 | def debug(cause: zio.Cause[String]): ZIO[Console, Nothing, Unit] = 63 | putStrLn(cause.prettyPrint) 64 | 65 | def takeNextMessageOrTimeout[A](id: PartId, queue: TQueue[A]): ZIO[Clock with Conf, String, A] = 66 | idleTTL flatMap queue.take.commit.timeoutFail(s"$id consumer expired") 67 | 68 | def safelyPerformAction[A](id: PartId, action: A => UIO[Unit])(a: A): ZIO[PartEnv, Nothing, Unit] = 69 | (userTTL flatMap (action(a).timeoutFail(s"$id action timed out")(_))).sandbox.catchAll(debug) 70 | 71 | def startConsumer[A]( 72 | id: PartId, 73 | queue: TQueue[A], 74 | cleanup: UIO[Unit], 75 | action: A => UIO[Unit] 76 | ): ZIO[PartEnv, Nothing, Unit] = 77 | (takeNextMessageOrTimeout(id, queue) flatMap safelyPerformAction(id, action)).forever.option 78 | .ensuring(cleanup) 79 | .fork 80 | .unit 81 | 82 | def hasConsumer[A](queues: Queues[A], id: PartId): STM[Nothing, Boolean] = 83 | queues.get.map(_.contains(id)) 84 | 85 | def removeConsumerFor[A](queues: Queues[A], id: PartId): UIO[Unit] = 86 | queues.update(_ - id).unit.commit 87 | 88 | def getWorkQueueFor[A](queues: Queues[A], id: PartId): STM[Nothing, TQueue[A]] = 89 | queues.get.map(_(id)) 90 | 91 | def setWorkQueueFor[A](queues: Queues[A], id: PartId, queue: TQueue[A]): STM[Nothing, Unit] = 92 | queues.update(_.updated(id, queue)).unit 93 | 94 | def createConsumer[A]( 95 | queues: Queues[A], 96 | id: PartId, 97 | maxPending: Int, 98 | action: A => UIO[Unit] 99 | ): STM[Nothing, ZIO[PartEnv, Nothing, Unit]] = 100 | for { 101 | queue <- TQueue.make[A](maxPending) 102 | _ <- setWorkQueueFor(queues, id, queue) 103 | } yield startConsumer(id, queue, removeConsumerFor(queues, id), action) 104 | 105 | def producer[A](queues: Queues[A], partIdOf: A => PartId, action: A => UIO[Unit])( 106 | a: A 107 | ): ZIO[PartEnv, Nothing, Boolean] = 108 | maxPending >>= { maxPending: Int => 109 | STM.atomically { 110 | for { 111 | exists <- hasConsumer(queues, partIdOf(a)) 112 | id = partIdOf(a) 113 | consumer <- if (exists) STM.succeed(ZIO.unit) else createConsumer(queues, id, maxPending, action) 114 | queue <- getWorkQueueFor(queues, partIdOf(a)) 115 | published <- publish(queue, a) 116 | } yield ZIO.succeed(published) <* consumer 117 | }.flatten 118 | } 119 | 120 | object Live extends Live 121 | } 122 | -------------------------------------------------------------------------------- /src/main/scala/freskog/concurrency/partition/package.scala: -------------------------------------------------------------------------------- 1 | package freskog.concurrency 2 | 3 | import zio.{ UIO, ZIO } 4 | 5 | package object partition extends Partition.Service[Partition] { 6 | 7 | type PartId = String 8 | 9 | final val partitionService: ZIO[Partition, Nothing, Partition.Service[Any]] = 10 | ZIO.access(_.partition) 11 | 12 | final def partition[A]( 13 | config: Config, 14 | partIdOf: A => PartId, 15 | action: A => UIO[Unit] 16 | ): ZIO[Partition, Nothing, A => UIO[Boolean]] = 17 | ZIO.accessM[Partition](_.partition.partition(config, partIdOf, action)) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/scala/freskog/concurrency/partition/BaseTest.scala: -------------------------------------------------------------------------------- 1 | package freskog.concurrency.partition 2 | 3 | import java.util.concurrent.TimeUnit.MILLISECONDS 4 | 5 | import zio._ 6 | import zio.console.{ Console } 7 | import zio.test._ 8 | import zio.test.Assertion.{ equalTo } 9 | import zio.test.TestAspect.{ timeout } 10 | import zio.stm.{ TQueue } 11 | import zio.duration._ 12 | import zio.clock.Clock 13 | 14 | import freskog.concurrency.partition._ 15 | import freskog.concurrency.partition.Partition._ 16 | import freskog.concurrency.partition.Common.{ PartEnv } 17 | 18 | object STMSpec 19 | extends ZIOBaseSpec( 20 | suite("STM spec")( 21 | suite("producer")( 22 | testM("return true when publishing to an empty TQueue") { 23 | (for { 24 | q <- TQueue.make[Int](1) 25 | published <- publish(q, 1) 26 | } yield assert(published, equalTo(true))).commit 27 | }, 28 | testM("return false when publishing to a full TQueue") { 29 | (for { 30 | q <- TQueue.make[Int](0) 31 | published <- publish(q, 1) 32 | } yield assert(published, equalTo(false))).commit 33 | } 34 | ), 35 | suite("consumer")( 36 | testM("return false when publishing to a full TQueue") { 37 | (for { 38 | q <- TQueue.make[Int](0) 39 | published <- publish(q, 1) 40 | } yield assert(published, equalTo(false))).commit 41 | } @@ timeout(1.nanos), 42 | testM("always successfully process a value on the queue") { 43 | val config = 44 | Config( 45 | userTTL = Duration(100, MILLISECONDS), 46 | idleTTL = Duration(100, MILLISECONDS), 47 | maxPending = 1 48 | ) 49 | 50 | def partEnv(config: Config): ZIO[Clock with Console, Nothing, PartEnv] = 51 | ZIO.environment[Clock with Console].map(Partition.buildEnv(config, _)) 52 | 53 | for { 54 | env <- partEnv(config) 55 | queue <- TQueue.make[String](1).commit 56 | promise <- Promise.make[Nothing, String] 57 | latch = promise.succeed(_: String).unit 58 | _ <- startConsumer("p1", queue, UIO.unit, latch).provide(env) 59 | _ <- queue.offer("published").commit 60 | result <- promise.await 61 | } yield assert(result, equalTo("published")) 62 | } @@ timeout(150.millis) 63 | ) 64 | ) 65 | ) 66 | -------------------------------------------------------------------------------- /src/test/scala/freskog/concurrency/partition/ZIOBaseSpec.scala: -------------------------------------------------------------------------------- 1 | package zio 2 | 3 | import zio.duration._ 4 | import zio.test.{ DefaultRunnableSpec, TestAspect, ZSpec } 5 | import zio.test.environment.TestEnvironment 6 | 7 | abstract class ZIOBaseSpec(spec: => ZSpec[TestEnvironment, Any, String, Any]) 8 | extends DefaultRunnableSpec(spec, List(TestAspect.timeout(60.seconds))) 9 | --------------------------------------------------------------------------------