Skip to content

Broadcast score group improvements - #19252

Merged
ornicar merged 6 commits into
lichess-org:masterfrom
allanjoseph98:scoreGroup-pt2
Jan 30, 2026
Merged

Broadcast score group improvements#19252
ornicar merged 6 commits into
lichess-org:masterfrom
allanjoseph98:scoreGroup-pt2

Conversation

@allanjoseph98

Copy link
Copy Markdown
Member
  1. Attempt to fix game sorting within scoregroups by guessing whether scoregroups are "parallel" or "sequential".
    • A "parallel" scoregroup is like the world rapid & blitz where rounds run simultaneously.
    • A "sequential" scoregroup is 99% of other scoregroups i.e. GCT Rapid & Blitz, Gotham vs Song. Each broadcast group happens one after another.
    • Games are currently sorted wrong for the sequential scoreGroups. I've attempted to solve this by inferring whether the tours are parallel or sequential by looking at the tour.dates start date and see if they're all similar.
  2. Calculate ratingDiffs and performance ratings per FideTC. For sequential scoregroups where the tournament format spans multiple time controls. Quite a common event format (think any Rapid+Blitz event).

@ornicar
ornicar merged commit 0e09d3a into lichess-org:master Jan 30, 2026
8 checks passed
@ornicar

ornicar commented Jan 30, 2026

Copy link
Copy Markdown
Collaborator

Am seeing many of these in prod:


! @8c8o6ic3n - Internal server error, for (GET) [/broadcast/bR0RpWOG/players] ->
 
play.api.UnexpectedException: Unexpected exception[NoSuchElementException: next on empty iterator]
        at play.api.UnexpectedException$.apply(Exceptions.scala:10)
        at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:328)
        at play.api.http.DefaultHttpErrorHandler.onServerError(HttpErrorHandler.scala:250)
        at play.core.server.netty.PlayRequestHandler$$anon$2.applyOrElse(PlayRequestHandler.scala:220)
        at play.core.server.netty.PlayRequestHandler$$anon$2.applyOrElse(PlayRequestHandler.scala:217)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:490)
        at play.api.libs.streams.Execution$trampoline$.execute(Execution.scala:65)
        at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:429)
        at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:338)
        at scala.concurrent.impl.Promise$DefaultPromise.tryComplete0(Promise.scala:285)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:504)
        at scala.concurrent.BatchingExecutor$AbstractBatch.runN(BatchingExecutor.scala:134)
        at scala.concurrent.BatchingExecutor$AsyncBatch.apply(BatchingExecutor.scala:163)
        at scala.concurrent.BatchingExecutor$AsyncBatch.apply(BatchingExecutor.scala:146)
        at scala.concurrent.BlockContext$.usingBlockContext(BlockContext.scala:107)
        at scala.concurrent.BatchingExecutor$AsyncBatch.run(BatchingExecutor.scala:154)
        at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinPool.scan(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(Unknown Source)
Caused by: java.util.NoSuchElementException: next on empty iterator
        at scala.collection.Iterator$$anon$19.next(Iterator.scala:973)
        at scala.collection.Iterator$$anon$19.next(Iterator.scala:971)
        at scala.collection.IterableOps.head(Iterable.scala:222)
        at scala.collection.IterableOps.head$(Iterable.scala:222)
        at scala.collection.AbstractIterable.head(Iterable.scala:935)
        at lila.relay.RelayPlayer$.given_OWrites_RelayPlayer$lzyINIT1$$anonfun$1$$anonfun$2(RelayPlayer.scala:151)
        at scala.Option.map(Option.scala:242)
        at lila.relay.RelayPlayer$.lila$relay$RelayPlayer$json$$$_$given_OWrites_RelayPlayer$lzyINIT1$$anonfun$1(RelayPlayer.scala:151)
        at play.api.libs.json.OWrites$$anon$4.writes(Writes.scala:150)
        at play.api.libs.json.OWrites$$anon$4.writes(Writes.scala:150)
        at play.api.libs.json.LowPriorityWrites.iterableWrites2$$anonfun$1$$anonfun$1(Writes.scala:534)
        at scala.collection.immutable.List.foreach(List.scala:334)
        at play.api.libs.json.LowPriorityWrites.iterableWrites2$$anonfun$1(Writes.scala:534)
        at play.api.libs.json.Writes$$anon$6.writes(Writes.scala:254)
        at play.api.libs.json.Json$.toJson(Json.scala:193)
        at lila.relay.RelayPlayerApi.$init$$$anonfun$2$$anonfun$1$$anonfun$1(RelayPlayer.scala:208)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:467)
        ... 11 common frames omitted
2026-01-30 07:23:04,389 ERROR http ERROR 500 RelayTour.playersView
play.api.UnexpectedException: Unexpected exception[NoSuchElementException: next on empty iterator]
        at play.api.UnexpectedException$.apply(Exceptions.scala:10)
        at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:328)
        at play.api.http.DefaultHttpErrorHandler.onServerError(HttpErrorHandler.scala:250)
        at play.core.server.netty.PlayRequestHandler$$anon$2.applyOrElse(PlayRequestHandler.scala:220)
        at play.core.server.netty.PlayRequestHandler$$anon$2.applyOrElse(PlayRequestHandler.scala:217)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:490)
        at play.api.libs.streams.Execution$trampoline$.execute(Execution.scala:65)
        at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:429)
        at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:338)
        at scala.concurrent.impl.Promise$DefaultPromise.tryComplete0(Promise.scala:285)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:504)
        at scala.concurrent.BatchingExecutor$AbstractBatch.runN(BatchingExecutor.scala:134)
        at scala.concurrent.BatchingExecutor$AsyncBatch.apply(BatchingExecutor.scala:163)
        at scala.concurrent.BatchingExecutor$AsyncBatch.apply(BatchingExecutor.scala:146)
        at scala.concurrent.BlockContext$.usingBlockContext(BlockContext.scala:107)
        at scala.concurrent.BatchingExecutor$AsyncBatch.run(BatchingExecutor.scala:154)
        at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinPool.scan(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(Unknown Source)
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(Unknown Source)
Caused by: java.util.NoSuchElementException: next on empty iterator
        at scala.collection.Iterator$$anon$19.next(Iterator.scala:973)
        at scala.collection.Iterator$$anon$19.next(Iterator.scala:971)
        at scala.collection.IterableOps.head(Iterable.scala:222)
        at scala.collection.IterableOps.head$(Iterable.scala:222)
        at scala.collection.AbstractIterable.head(Iterable.scala:935)
        at lila.relay.RelayPlayer$.given_OWrites_RelayPlayer$lzyINIT1$$anonfun$1$$anonfun$2(RelayPlayer.scala:151)
        at scala.Option.map(Option.scala:242)
        at lila.relay.RelayPlayer$.lila$relay$RelayPlayer$json$$$_$given_OWrites_RelayPlayer$lzyINIT1$$anonfun$1(RelayPlayer.scala:151)
        at play.api.libs.json.OWrites$$anon$4.writes(Writes.scala:150)
        at play.api.libs.json.OWrites$$anon$4.writes(Writes.scala:150)
        at play.api.libs.json.LowPriorityWrites.iterableWrites2$$anonfun$1$$anonfun$1(Writes.scala:534)
        at scala.collection.immutable.List.foreach(List.scala:334)
        at play.api.libs.json.LowPriorityWrites.iterableWrites2$$anonfun$1(Writes.scala:534)
        at play.api.libs.json.Writes$$anon$6.writes(Writes.scala:254)
        at play.api.libs.json.Json$.toJson(Json.scala:193)
        at lila.relay.RelayPlayerApi.$init$$$anonfun$2$$anonfun$1$$anonfun$1(RelayPlayer.scala:208)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:467)
        ... 11 common frames omitted

@ornicar

ornicar commented Jan 30, 2026

Copy link
Copy Markdown
Collaborator
        .add("ratingDiff" -> p.ratingDiffs.map(_.head._2)) // API BC grace

OMG a .head that I didn't spot in my review.

@allanjoseph98

Copy link
Copy Markdown
Member Author

My mistake. I had switched from NonEmptyMap where it was safe to Map and this remained

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants