Skip to content

Commit 388764d

Browse files
adamgfraserguersam
andauthored
Implement ZLayer#reloadable (zio#8077)
* implement zlayer reloadable * fix compilation error * mark test as flaky * fix compilation error * move to test package * Turn `ServiceProxy` into a typeclass-like structure * Fix ServiceReloader * format * move to zio config * move service reloader to macros project * implement zlayer reloadable * rename servicy proxy to is reloadable * rename generate to reloadable * update documentation * add test * fix scala 3 issue --------- Co-authored-by: Jisoo Park <[email protected]>
1 parent dd68ee8 commit 388764d

File tree

12 files changed

+432
-134
lines changed

12 files changed

+432
-134
lines changed

core-tests/shared/src/test/scala/zio/ServiceProxySpec.scala core-tests/shared/src/test/scala/zio/IsReloadableSpec.scala

+66-66
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,21 @@ package zio
1919
import zio.test._
2020

2121
@scala.annotation.experimental
22-
object ServiceProxySpec extends ZIOSpecDefault {
22+
object IsReloadableSpec extends ZIOSpecDefault {
2323

24-
val spec = suite("ServiceProxy")(
25-
suite("generates a proxy")(
24+
val spec = suite("IsReloadable")(
25+
suite("creates a reloadable service")(
2626
test("switches underlying service in runtime") {
2727
trait Foo { def bar: UIO[String] }
2828

2929
val service1: Foo = new Foo { def bar = ZIO.succeed("zio1") }
3030
val service2: Foo = new Foo { def bar = ZIO.succeed("zio2") }
3131
for {
32-
ref <- ScopedRef.make(service1)
33-
proxy = ServiceProxy.generate(ref)
34-
res1 <- proxy.bar
35-
_ <- ref.set(ZIO.succeed(service2))
36-
res2 <- proxy.bar
32+
ref <- ScopedRef.make(service1)
33+
reloadable = IsReloadable[Foo].reloadable(ref)
34+
res1 <- reloadable.bar
35+
_ <- ref.set(ZIO.succeed(service2))
36+
res2 <- reloadable.bar
3737
} yield assertTrue(res1 == "zio1" && res2 == "zio2")
3838
},
3939
test("the type out of the current lexical scope") {
@@ -42,9 +42,9 @@ object ServiceProxySpec extends ZIOSpecDefault {
4242
}
4343
val service: foo.Foo = new foo.Foo { def bar = ZIO.succeed("zio") }
4444
for {
45-
ref <- ScopedRef.make(service)
46-
proxy = ServiceProxy.generate(ref)
47-
res1 <- proxy.bar
45+
ref <- ScopedRef.make(service)
46+
reloadable = IsReloadable[foo.Foo].reloadable(ref)
47+
res1 <- reloadable.bar
4848
} yield assertTrue(res1 == "zio")
4949
},
5050
test("multiple methods") {
@@ -57,37 +57,37 @@ object ServiceProxySpec extends ZIOSpecDefault {
5757
override def baz: UIO[Int] = ZIO.succeed(1)
5858
}
5959
for {
60-
ref <- ScopedRef.make(service)
61-
proxy = ServiceProxy.generate(ref)
62-
res1 <- proxy.bar
63-
res2 <- proxy.baz
60+
ref <- ScopedRef.make(service)
61+
reloadable = IsReloadable[Foo].reloadable(ref)
62+
res1 <- reloadable.bar
63+
res2 <- reloadable.baz
6464
} yield assertTrue(res1 == "zio" && res2 == 1)
6565
},
6666
test("trait with type parameter") {
6767
trait Foo[A] { def bar: UIO[A] }
6868
val service: Foo[String] = new Foo[String] { def bar = ZIO.succeed("baz") }
6969
for {
70-
ref <- ScopedRef.make(service)
71-
proxy = ServiceProxy.generate(ref)
72-
res <- proxy.bar
70+
ref <- ScopedRef.make(service)
71+
reloadable = IsReloadable[Foo[String]].reloadable(ref)
72+
res <- reloadable.bar
7373
} yield assertTrue(res == "baz")
7474
},
7575
test("trait with higher kinded type parameter") {
7676
trait Foo[F[_]] { def bar: UIO[F[String]] }
7777
val service: Foo[List] = new Foo[List] { def bar = ZIO.succeed("baz" :: Nil) }
7878
for {
79-
ref <- ScopedRef.make(service)
80-
proxy = ServiceProxy.generate(ref)
81-
res <- proxy.bar
79+
ref <- ScopedRef.make(service)
80+
reloadable = IsReloadable[Foo[List]].reloadable(ref)
81+
res <- reloadable.bar
8282
} yield assertTrue(res == "baz" :: Nil)
8383
},
8484
test("abstract class") {
8585
abstract class Foo { def bar: UIO[String] }
8686
val service: Foo = new Foo { def bar = ZIO.succeed("zio") }
8787
for {
88-
ref <- ScopedRef.make(service)
89-
proxy = ServiceProxy.generate(ref)
90-
res <- proxy.bar
88+
ref <- ScopedRef.make(service)
89+
reloadable = IsReloadable[Foo].reloadable(ref)
90+
res <- reloadable.bar
9191
} yield assertTrue(res == "zio")
9292
},
9393
test("class") {
@@ -96,9 +96,9 @@ object ServiceProxySpec extends ZIOSpecDefault {
9696
override def bar = ZIO.succeed("zio2")
9797
}
9898
for {
99-
ref <- ScopedRef.make(service)
100-
proxy = ServiceProxy.generate(ref)
101-
res <- proxy.bar
99+
ref <- ScopedRef.make(service)
100+
reloadable = IsReloadable[Foo].reloadable(ref)
101+
res <- reloadable.bar
102102
} yield assertTrue(res == "zio2")
103103
}
104104
),
@@ -107,37 +107,37 @@ object ServiceProxySpec extends ZIOSpecDefault {
107107
trait Foo { def bar(a: String): UIO[Int] }
108108
val service: Foo = new Foo { def bar(a: String) = ZIO.succeed(a.length) }
109109
for {
110-
ref <- ScopedRef.make(service)
111-
proxy = ServiceProxy.generate(ref)
112-
res <- proxy.bar("zio")
110+
ref <- ScopedRef.make(service)
111+
reloadable = IsReloadable[Foo].reloadable(ref)
112+
res <- reloadable.bar("zio")
113113
} yield assertTrue(res == 3)
114114
},
115115
test("generic methods") {
116116
trait Foo { def bar[A](a: A): UIO[A] }
117117
val service: Foo = new Foo { def bar[A](a: A) = ZIO.succeed(a) }
118118
for {
119-
ref <- ScopedRef.make(service)
120-
proxy = ServiceProxy.generate(ref)
121-
res <- proxy.bar[String]("zio")
119+
ref <- ScopedRef.make(service)
120+
reloadable = IsReloadable[Foo].reloadable(ref)
121+
res <- reloadable.bar[String]("zio")
122122
} yield assertTrue(res == "zio")
123123
},
124124
test("curried methods") {
125125
trait Foo { def bar(a: Int)(b: String): UIO[String] }
126126
val service: Foo = new Foo { def bar(a: Int)(b: String) = ZIO.succeed(b * a) }
127127
for {
128-
ref <- ScopedRef.make(service)
129-
proxy = ServiceProxy.generate(ref)
130-
res <- proxy.bar(3)("zio")
128+
ref <- ScopedRef.make(service)
129+
reloadable = IsReloadable[Foo].reloadable(ref)
130+
res <- reloadable.bar(3)("zio")
131131
} yield assertTrue(res == "zioziozio")
132132
},
133133
test("implicit clauses") {
134134
trait Foo { def bar(a: Int)(implicit b: String): UIO[String] }
135135
val service: Foo = new Foo { def bar(a: Int)(implicit b: String) = ZIO.succeed(b * a) }
136136
implicit val b: String = "zio"
137137
for {
138-
ref <- ScopedRef.make(service)
139-
proxy = ServiceProxy.generate(ref)
140-
res <- proxy.bar(3)
138+
ref <- ScopedRef.make(service)
139+
reloadable = IsReloadable[Foo].reloadable(ref)
140+
res <- reloadable.bar(3)
141141
} yield assertTrue(res == "zioziozio")
142142
},
143143
test("inherited abstract methods") {
@@ -146,48 +146,48 @@ object ServiceProxySpec extends ZIOSpecDefault {
146146

147147
val service: Foo = new Foo { def bar(a: Int) = ZIO.succeed("zio" * a) }
148148
for {
149-
ref <- ScopedRef.make(service)
150-
proxy = ServiceProxy.generate(ref)
151-
res <- proxy.bar(3)
149+
ref <- ScopedRef.make(service)
150+
reloadable = IsReloadable[Foo].reloadable(ref)
151+
res <- reloadable.bar(3)
152152
} yield assertTrue(res == "zioziozio")
153153
},
154154
test("overridden methods with default implementation") {
155155
trait Foo { def bar: UIO[String] = ZIO.succeed("zio1") }
156156
val service: Foo = new Foo { override def bar = ZIO.succeed("zio2") }
157157
for {
158-
ref <- ScopedRef.make(service)
159-
proxy = ServiceProxy.generate(ref)
160-
res <- proxy.bar
158+
ref <- ScopedRef.make(service)
159+
reloadable = IsReloadable[Foo].reloadable(ref)
160+
res <- reloadable.bar
161161
} yield assertTrue(res == "zio2")
162162
},
163163
test("package private methods") {
164164
trait Foo { private[zio] def bar: UIO[String] }
165165
val service: Foo = new Foo { private[zio] def bar: UIO[String] = ZIO.succeed("zio") }
166166
for {
167-
ref <- ScopedRef.make(service)
168-
proxy = ServiceProxy.generate(ref)
169-
res <- proxy.bar
167+
ref <- ScopedRef.make(service)
168+
reloadable = IsReloadable[Foo].reloadable(ref)
169+
res <- reloadable.bar
170170
} yield assertTrue(res == "zio")
171171
},
172172
test("ZIO vals") {
173173
trait Foo { val bar: UIO[String] }
174174
val service1: Foo = new Foo { val bar: UIO[String] = ZIO.succeed("zio1") }
175175
val service2: Foo = new Foo { val bar: UIO[String] = ZIO.succeed("zio2") }
176176
for {
177-
ref <- ScopedRef.make(service1)
178-
proxy = ServiceProxy.generate(ref)
179-
res1 <- proxy.bar
180-
_ <- ref.set(ZIO.succeed(service2))
181-
res2 <- proxy.bar
177+
ref <- ScopedRef.make(service1)
178+
reloadable = IsReloadable[Foo].reloadable(ref)
179+
res1 <- reloadable.bar
180+
_ <- ref.set(ZIO.succeed(service2))
181+
res2 <- reloadable.bar
182182
} yield assertTrue(res1 == "zio1" && res2 == "zio2")
183183
},
184184
test("ZIO vals with default implementation") {
185185
trait Foo { val bar: UIO[String] = ZIO.succeed("zio") }
186186
val service: Foo = new Foo { override val bar: UIO[String] = ZIO.succeed("zio1") }
187187
for {
188-
ref <- ScopedRef.make(service)
189-
proxy = ServiceProxy.generate(ref)
190-
res1 <- proxy.bar
188+
ref <- ScopedRef.make(service)
189+
reloadable = IsReloadable[Foo].reloadable(ref)
190+
res1 <- reloadable.bar
191191
} yield assertTrue(res1 == "zio1")
192192
},
193193
test("keeps non-ZIO default implementations") {
@@ -197,9 +197,9 @@ object ServiceProxySpec extends ZIOSpecDefault {
197197
}
198198
val service: Foo = new Foo { def bar = ZIO.succeed("baz") }
199199
for {
200-
ref <- ScopedRef.make(service)
201-
proxy = ServiceProxy.generate(ref)
202-
} yield assertTrue(proxy.qux == "quux")
200+
ref <- ScopedRef.make(service)
201+
reloadable = IsReloadable[Foo].reloadable(ref)
202+
} yield assertTrue(reloadable.qux == "quux")
203203
}
204204
),
205205
suite("fails to compile")(
@@ -211,7 +211,7 @@ object ServiceProxySpec extends ZIOSpecDefault {
211211
val service: Foo = new Foo { def qux = "quux" }
212212
for {
213213
ref <- ScopedRef.make(service)
214-
} yield ServiceProxy.generate(ref)
214+
} yield IsReloadable[Foo].reloadable(ref)
215215
"""
216216
)
217217
} yield
@@ -227,9 +227,9 @@ object ServiceProxySpec extends ZIOSpecDefault {
227227
class Foo(s: String) { def bar: UIO[String] = ZIO.succeed(s) }
228228
val service: Foo = new Foo("zio")
229229
for {
230-
ref <- ScopedRef.make(service)
231-
proxy = ServiceProxy.generate(ref)
232-
res <- proxy.bar
230+
ref <- ScopedRef.make(service)
231+
reloadable = IsReloadable[Foo].reloadable(ref)
232+
res <- reloadable.bar
233233
} yield res
234234
"""
235235
)
@@ -253,9 +253,9 @@ object ServiceProxySpec extends ZIOSpecDefault {
253253
def bar: UIO[String] = ZIO.succeed("zio")
254254
}
255255
for {
256-
ref <- ScopedRef.make(service)
257-
proxy = ServiceProxy.generate(ref)
258-
res <- proxy.bar
256+
ref <- ScopedRef.make(service)
257+
reloadable = IsReloadable[Foo].reloadable(ref)
258+
res <- reloadable.bar
259259
} yield assertTrue(res == "zio")
260260
"""
261261
)

core/shared/src/main/scala-2/zio/ServiceProxyVersionSpecific.scala core/shared/src/main/scala-2/zio/IsReloadableVersionSpecific.scala

+3-6
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,19 @@
1616

1717
package zio
1818

19-
import zio.internal.macros.ServiceProxyMacros
19+
import zio.internal.macros.IsReloadableMacros
2020

21-
trait ServiceProxyVersionSpecific {
21+
trait IsReloadableVersionSpecific {
2222

2323
/**
2424
* Generates a proxy instance of the specified service.
2525
*
2626
* @tparam A
2727
* The type of the service.
28-
* @param service
29-
* The [[zio.ScopedRef]] containing the service for which a proxy is to be
30-
* generated.
3128
* @return
3229
* A proxy instance of the service that forwards ZIO method calls to the
3330
* underlying service and allows the service to change its behavior at
3431
* runtime.
3532
*/
36-
def generate[A](service: ScopedRef[A]): A = macro ServiceProxyMacros.makeImpl[A]
33+
implicit def derived[A]: IsReloadable[A] = macro IsReloadableMacros.makeImpl[A]
3734
}

core/shared/src/main/scala-2/zio/internal/macros/ServiceProxyMacros.scala core/shared/src/main/scala-2/zio/internal/macros/IsReloadableMacros.scala

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package zio.internal.macros
22

3-
import zio.ScopedRef
3+
import zio.{IsReloadable, ScopedRef}
44
import scala.reflect.macros.blackbox
55

6-
class ServiceProxyMacros(val c: blackbox.Context) {
6+
class IsReloadableMacros(val c: blackbox.Context) {
77
import c.universe._
8-
def makeImpl[A: c.WeakTypeTag](service: c.Expr[ScopedRef[A]]): c.Expr[A] = {
8+
9+
def makeImpl[A: c.WeakTypeTag]: c.Expr[IsReloadable[A]] = {
910
val tpe = c.weakTypeOf[A]
1011

1112
def unsupported(reason: String): Nothing =
@@ -25,7 +26,7 @@ class ServiceProxyMacros(val c: blackbox.Context) {
2526
def defect(reason: String): Nothing =
2627
c.abort(
2728
c.enclosingPosition,
28-
s"""Defect in zio.ServiceProxy:
29+
s"""Defect in zio.IsReloadable:
2930
|
3031
| $reason""".stripMargin
3132
)
@@ -45,6 +46,7 @@ class ServiceProxyMacros(val c: blackbox.Context) {
4546
}
4647

4748
val resultType = appliedType(tpe.typeConstructor, tpe.typeArgs)
49+
4850
val forwarders = tpe.members.view
4951
.filter(m => m.isTerm && (m.asMethod.returnType <:< c.weakTypeOf[zio.ZIO[_, _, _]]))
5052
.map { sym =>
@@ -60,7 +62,7 @@ class ServiceProxyMacros(val c: blackbox.Context) {
6062

6163
val args = m.paramLists.map(_.map(p => p.name.toTermName))
6264

63-
val rhs = q"${service.tree}.get.flatMap(_.${sym.name.toTermName}(...$args))"
65+
val rhs = q"_$$scopedRef.get.flatMap(_.${sym.name.toTermName}(...$args))"
6466
sym.asTerm match {
6567
case t if t.isVal =>
6668
q"override val ${sym.name.toTermName}: ${m.finalResultType} = $rhs"
@@ -72,7 +74,14 @@ class ServiceProxyMacros(val c: blackbox.Context) {
7274
}
7375
.toList
7476

75-
c.Expr(q"new $resultType { ..$forwarders }")
77+
c.Expr[IsReloadable[A]](
78+
q"""
79+
new _root_.zio.IsReloadable[$resultType] {
80+
def reloadable(_$$scopedRef: _root_.zio.ScopedRef[$resultType]): $resultType =
81+
new $resultType { ..$forwarders }
82+
}
83+
"""
84+
)
7685
}
7786

7887
}

core/shared/src/main/scala-3/zio/ServiceProxyVersionSpecific.scala core/shared/src/main/scala-3/zio/IsReloadableVersionSpecific.scala

+13-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package zio
1919
import scala.annotation.experimental
2020
import scala.quoted.*
2121

22-
trait ServiceProxyVersionSpecific {
22+
trait IsReloadableVersionSpecific {
2323

2424
/**
2525
* Generates a proxy instance of the specified service.
@@ -30,10 +30,19 @@ trait ServiceProxyVersionSpecific {
3030
* and allows the service to change its behavior at runtime.
3131
*/
3232
@experimental
33-
inline def generate[A](service: ScopedRef[A]): A = ${ ServiceProxyMacros.makeImpl('service) }
33+
inline given derived[A]: IsReloadable[A] = ${ IsReloadableMacros.derive[A] }
3434
}
3535

36-
private object ServiceProxyMacros {
36+
private object IsReloadableMacros {
37+
38+
@experimental
39+
def derive[A: Type](using Quotes): Expr[IsReloadable[A]] =
40+
'{
41+
new IsReloadable[A] {
42+
override def reloadable(scopedRef: ScopedRef[A]): A =
43+
${ makeImpl('scopedRef) }
44+
}
45+
}
3746

3847
@experimental
3948
def makeImpl[A: Type](service: Expr[ScopedRef[A]])(using Quotes): Expr[A] = {
@@ -64,7 +73,7 @@ private object ServiceProxyMacros {
6473

6574
def defect(reason: String): Nothing =
6675
report.errorAndAbort(
67-
s"""Defect in zio.ServiceProxy:
76+
s"""Defect in zio.IsReloadable:
6877
|
6978
| $reason""".stripMargin
7079
)

0 commit comments

Comments
 (0)