Skip to content

Commit ce0ddb9

Browse files
adamwclaude
andcommitted
Inline try* helpers; fix CircuitBreakerTest flakiness; use in: style
- Remove fromJoxTryReceiveOrClosed/fromJoxTrySendOrClosed helper methods and inline their logic in tryReceiveOrClosed/trySendOrClosed, delegating to the existing fromJoxOrT and fromJox methods. Expose fromJox as private[ox] to enable this. - Update ChannelTryTest to use Scala 3 in: syntax (no braces) - Fix CircuitBreakerTest flakiness by wrapping transition assertions in eventually{}, giving a 1s window for actor-dispatched state changes to propagate under JVM load Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ccfec2c commit ce0ddb9

4 files changed

Lines changed: 47 additions & 76 deletions

File tree

core/src/main/scala/ox/channels/Channel.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,12 @@ trait Source[+T] extends SourceOps[T] with SourceDrainOps[T]:
7676
* [[Some]] with the received value, [[None]] if no value is immediately available, or [[ChannelClosed]] when the channel is closed.
7777
*/
7878
def tryReceiveOrClosed(): Option[T] | ChannelClosed =
79-
ChannelClosed.fromJoxTryReceiveOrClosed(delegate.tryReceiveOrClosed())
79+
val r = delegate.tryReceiveOrClosed()
80+
if r == null then None
81+
else
82+
ChannelClosed.fromJoxOrT[T](r.asInstanceOf[AnyRef]) match
83+
case c: ChannelClosed => c
84+
case v: T @unchecked => Some(v)
8085

8186
/** Receive a value from the channel. For a variant which throws exceptions when the channel is closed, use [[receive]].
8287
*
@@ -176,7 +181,12 @@ trait Sink[-T]:
176181
* `true` if the value was sent, `false` if there's no space/waiting receiver, or [[ChannelClosed]] when the channel is closed.
177182
*/
178183
def trySendOrClosed(t: T): Boolean | ChannelClosed =
179-
ChannelClosed.fromJoxTrySendOrClosed(delegate.asInstanceOf[JSink[T]].trySendOrClosed(t))
184+
val r = delegate.asInstanceOf[JSink[T]].trySendOrClosed(t)
185+
if r == null then true // null = sent
186+
else
187+
ChannelClosed.fromJox(r.asInstanceOf[AnyRef]) match
188+
case c: ChannelClosed => c
189+
case _ => false // sentinel = not sent
180190

181191
/** Send a value to the channel. For a variant which throws exceptions when the channel is closed, use [[send]].
182192
*

core/src/main/scala/ox/channels/ChannelClosed.scala

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,7 @@ object ChannelClosed:
1616
private[ox] def fromJoxOrUnit(joxResult: AnyRef): Unit | ChannelClosed =
1717
if joxResult == null then () else fromJox(joxResult).asInstanceOf[ChannelClosed]
1818

19-
/** Converts the result of jox's `tryReceiveOrClosed()` (which returns `T | JChannelClosed | null`) to `Option[T] | ChannelClosed`. `null`
20-
* means no value was immediately available.
21-
*/
22-
private[ox] def fromJoxTryReceiveOrClosed[T](joxResult: AnyRef): Option[T] | ChannelClosed =
23-
joxResult match
24-
case null => None
25-
case _: JChannelDone => Done
26-
case e: JChannelError => Error(e.cause())
27-
case v => Some(v.asInstanceOf[T])
28-
29-
/** Converts the result of jox's `trySendOrClosed()` (which returns `null | JChannelClosed | sentinel`) to `Boolean | ChannelClosed`.
30-
* `null` means the value was sent, any other non-`ChannelClosed` value (sentinel) means it was not sent.
31-
*/
32-
private[ox] def fromJoxTrySendOrClosed(joxResult: AnyRef): Boolean | ChannelClosed =
33-
joxResult match
34-
case null => true
35-
case _: JChannelDone => Done
36-
case e: JChannelError => Error(e.cause())
37-
case _ => false
38-
39-
private def fromJox(joxResult: AnyRef): AnyRef | ChannelClosed =
19+
private[ox] def fromJox(joxResult: AnyRef): AnyRef | ChannelClosed =
4020
joxResult match
4121
case _: JChannelDone => Done
4222
case e: JChannelError => Error(e.cause())

core/src/test/scala/ox/channels/ChannelTryTest.scala

Lines changed: 22 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,129 +6,107 @@ import org.scalatest.matchers.should.Matchers
66
class ChannelTryTest extends AnyFlatSpec with Matchers:
77
// trySend
88

9-
"trySend" should "return true when there is space in the buffer" in {
9+
"trySend" should "return true when there is space in the buffer" in:
1010
val c = Channel.buffered[Int](2)
1111
c.trySend(1) shouldBe true
1212
c.trySend(2) shouldBe true
13-
}
1413

15-
it should "return false when the buffer is full" in {
14+
it should "return false when the buffer is full" in:
1615
val c = Channel.buffered[Int](1)
1716
c.trySend(1) shouldBe true
1817
c.trySend(2) shouldBe false
19-
}
2018

21-
it should "return true for an unlimited channel" in {
19+
it should "return true for an unlimited channel" in:
2220
val c = Channel.unlimited[Int]
2321
c.trySend(1) shouldBe true
2422
c.trySend(2) shouldBe true
25-
}
2623

27-
it should "throw ChannelClosedException.Done when the channel is done" in {
24+
it should "throw ChannelClosedException.Done when the channel is done" in:
2825
val c = Channel.buffered[Int](2)
2926
c.done()
30-
assertThrows[ChannelClosedException.Done] {
27+
assertThrows[ChannelClosedException.Done]:
3128
c.trySend(1)
32-
}
33-
}
3429

35-
it should "throw ChannelClosedException.Error when the channel is in error" in {
30+
it should "throw ChannelClosedException.Error when the channel is in error" in:
3631
val c = Channel.buffered[Int](2)
3732
c.error(new RuntimeException("test"))
38-
assertThrows[ChannelClosedException.Error] {
33+
assertThrows[ChannelClosedException.Error]:
3934
c.trySend(1)
40-
}
41-
}
4235

4336
// trySendOrClosed
4437

45-
"trySendOrClosed" should "return true when there is space in the buffer" in {
38+
"trySendOrClosed" should "return true when there is space in the buffer" in:
4639
val c = Channel.buffered[Int](2)
4740
c.trySendOrClosed(1) shouldBe true
4841
c.trySendOrClosed(2) shouldBe true
49-
}
5042

51-
it should "return false when the buffer is full" in {
43+
it should "return false when the buffer is full" in:
5244
val c = Channel.buffered[Int](1)
5345
c.trySendOrClosed(1) shouldBe true
5446
c.trySendOrClosed(2) shouldBe false
55-
}
5647

57-
it should "return ChannelClosed.Done when the channel is done" in {
48+
it should "return ChannelClosed.Done when the channel is done" in:
5849
val c = Channel.buffered[Int](2)
5950
c.done()
6051
c.trySendOrClosed(1) shouldBe ChannelClosed.Done
61-
}
6252

63-
it should "return ChannelClosed.Error when the channel is in error" in {
53+
it should "return ChannelClosed.Error when the channel is in error" in:
6454
val c = Channel.buffered[Int](2)
6555
val ex = new RuntimeException("test")
6656
c.error(ex)
6757
c.trySendOrClosed(1) should matchPattern { case ChannelClosed.Error(_) => }
68-
}
6958

7059
// tryReceive
7160

72-
"tryReceive" should "return Some with the value when one is available" in {
61+
"tryReceive" should "return Some with the value when one is available" in:
7362
val c = Channel.buffered[Int](2)
7463
c.send(1)
7564
c.send(2)
7665
c.tryReceive() shouldBe Some(1)
7766
c.tryReceive() shouldBe Some(2)
78-
}
7967

80-
it should "return None when the buffer is empty" in {
68+
it should "return None when the buffer is empty" in:
8169
val c = Channel.buffered[Int](2)
8270
c.tryReceive() shouldBe None
83-
}
8471

85-
it should "throw ChannelClosedException.Done when the channel is done and empty" in {
72+
it should "throw ChannelClosedException.Done when the channel is done and empty" in:
8673
val c = Channel.buffered[Int](2)
8774
c.done()
88-
assertThrows[ChannelClosedException.Done] {
75+
assertThrows[ChannelClosedException.Done]:
8976
c.tryReceive()
90-
}
91-
}
9277

93-
it should "throw ChannelClosedException.Error when the channel is in error" in {
78+
it should "throw ChannelClosedException.Error when the channel is in error" in:
9479
val c = Channel.buffered[Int](2)
9580
c.send(1)
9681
c.error(new RuntimeException("test"))
97-
assertThrows[ChannelClosedException.Error] {
82+
assertThrows[ChannelClosedException.Error]:
9883
c.tryReceive()
99-
}
100-
}
10184

10285
// tryReceiveOrClosed
10386

104-
"tryReceiveOrClosed" should "return Some with the value when one is available" in {
87+
"tryReceiveOrClosed" should "return Some with the value when one is available" in:
10588
val c = Channel.buffered[Int](2)
10689
c.send(1)
10790
c.tryReceiveOrClosed() shouldBe Some(1)
108-
}
10991

110-
it should "return None when the buffer is empty" in {
92+
it should "return None when the buffer is empty" in:
11193
val c = Channel.buffered[Int](2)
11294
c.tryReceiveOrClosed() shouldBe None
113-
}
11495

115-
it should "return ChannelClosed.Done when the channel is done and empty" in {
96+
it should "return ChannelClosed.Done when the channel is done and empty" in:
11697
val c = Channel.buffered[Int](2)
11798
c.done()
11899
c.tryReceiveOrClosed() shouldBe ChannelClosed.Done
119-
}
120100

121-
it should "return ChannelClosed.Error when the channel is in error" in {
101+
it should "return ChannelClosed.Error when the channel is in error" in:
122102
val c = Channel.buffered[Int](2)
123103
c.send(1)
124104
c.error(new RuntimeException("test"))
125105
c.tryReceiveOrClosed() should matchPattern { case ChannelClosed.Error(_) => }
126-
}
127106

128-
it should "return Some(value) from a done channel that still has buffered values" in {
107+
it should "return Some(value) from a done channel that still has buffered values" in:
129108
val c = Channel.buffered[Int](2)
130109
c.send(42)
131110
c.done()
132111
c.tryReceiveOrClosed() shouldBe Some(42)
133-
}
134112
end ChannelTryTest

core/src/test/scala/ox/resilience/CircuitBreakerTest.scala

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ package ox.resilience
33
import org.scalatest.flatspec.AnyFlatSpec
44
import org.scalatest.matchers.should.Matchers
55
import org.scalatest.OptionValues
6+
import org.scalatest.concurrent.Eventually
7+
import org.scalatest.time.{Millis, Seconds, Span}
68
import scala.concurrent.duration.*
79
import ox.*
810
import org.scalatest.EitherValues
911

10-
class CircuitBreakerTest extends AnyFlatSpec with Matchers with OptionValues with EitherValues:
12+
class CircuitBreakerTest extends AnyFlatSpec with Matchers with OptionValues with EitherValues with Eventually:
13+
implicit override val patienceConfig: PatienceConfig =
14+
PatienceConfig(timeout = Span(1, Seconds), interval = Span(50, Millis))
1115
behavior of "Circuit Breaker run operations"
1216

1317
it should "run operation when metrics are not exceeded" in supervised {
@@ -190,23 +194,23 @@ class CircuitBreakerTest extends AnyFlatSpec with Matchers with OptionValues wit
190194

191195
// 750ms: the first operation failed, should be open
192196
sleep(500.millis)
193-
circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.Open]
197+
eventually { circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.Open] }
194198

195199
// 1750ms: first operation failed more than 1s ago, second operation failed less than 1s ago and was ignored
196200
sleep(1.second)
197-
circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.HalfOpen]
201+
eventually { circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.HalfOpen] }
198202

199203
// 2250ms: more than 1s after the last failing operation, should be now half-open
200204
sleep(500.millis)
201205
circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.HalfOpen]
202206

203207
// 3250ms: at 2500ms 1 sec timeout on halfOpen state passes, we go back to open
204208
sleep(1.second)
205-
circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.Open]
209+
eventually { circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.Open] }
206210

207211
// 3750ms: at 3500ms we go to halfOpen again
208212
sleep(1000.millis)
209-
circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.HalfOpen]
213+
eventually { circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.HalfOpen] }
210214
}
211215

212216
it should "correctly calculate metrics when results come in after state change" in supervised {
@@ -246,18 +250,17 @@ class CircuitBreakerTest extends AnyFlatSpec with Matchers with OptionValues wit
246250

247251
// 750ms: the first operation failed, should be open
248252
sleep(500.millis)
249-
circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.Open]
253+
eventually { circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.Open] }
250254

251255
// 1750ms: first operation failed more than 1s ago, second operation failed less than 1s ago and was ignored
252256
sleep(1.second)
253-
circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.HalfOpen]
257+
eventually { circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.HalfOpen] }
254258

255259
// 2250ms: complete enough operations for halfOpen state - since success should switch back to Closed
256260
sleep(500.millis)
257261
circuitBreaker.runOrDropEither(Right("c")).discard
258-
sleep(100.millis) // wait for state to register
259262
// Should go back to closed, we have one successful operation
260-
circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.Closed]
263+
eventually { circuitBreaker.stateMachine.state shouldBe a[CircuitBreakerState.Closed] }
261264
}
262265

263266
end CircuitBreakerTest

0 commit comments

Comments
 (0)