diff --git a/build.gradle b/build.gradle index c3d39c3..22da9ba 100644 --- a/build.gradle +++ b/build.gradle @@ -83,20 +83,25 @@ dependencies { implementation group: 'cc.sukazyo', name: 'messiva', version: lib_messiva_v implementation group: 'cc.sukazyo', name: 'resource-tools', version: lib_resourcetools_v - testImplementation group: 'cc.sukazyo', name: 'resource-tools', version: lib_resourcetools_v implementation group: 'com.github.pengrad', name: 'java-telegram-bot-api', version: lib_javatelegramapi_v implementation group: 'com.softwaremill.sttp.client3', name: scala('core'), version: lib_sttp_v implementation group: 'com.softwaremill.sttp.client3', name: scala('okhttp-backend'), version: lib_sttp_v - implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: lib_okhttp_v + runtimeOnly group: 'com.squareup.okhttp3', name: 'okhttp', version: lib_okhttp_v implementation group: 'com.google.code.gson', name: 'gson', version: lib_gson_v implementation group: 'com.cronutils', name: 'cron-utils', version: lib_cron_utils_v + // used for disable slf4j + // due to the slf4j api have been used in the following libraries: + // - cron-utils + runtimeOnly group: 'org.slf4j', name: 'slf4j-nop', version: lib_slf4j_v + testRuntimeOnly group: 'org.slf4j', name: 'slf4j-nop', version: lib_slf4j_v + + testImplementation group: 'cc.sukazyo', name: 'resource-tools', version: lib_resourcetools_v testImplementation group: 'org.scalatest', name: scala('scalatest'), version: lib_scalatest_v testImplementation group: 'org.scalatest', name: scala('scalatest-freespec'), version: lib_scalatest_v testRuntimeOnly group: 'org.scala-lang.modules', name: scala('scala-xml'), version: lib_scalamodule_xml_v - - // for generating HTML report // required by gradle-scalatest plugin + // for generating HTML report: required by gradle-scalatest plugin testRuntimeOnly group: 'com.vladsch.flexmark', name: 'flexmark-all', version: '0.64.6' } diff --git a/gradle.properties b/gradle.properties index 9dcc505..70ddbdf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.3.0-dev2 +VERSION = 1.3.0-dev3 USE_DELTA = false VERSION_DELTA = @@ -19,6 +19,7 @@ lib_scalamodule_xml_v = 2.2.0 lib_messiva_v = 0.2.0 lib_resourcetools_v = 0.2.2 +lib_slf4j_v = 2.0.9 lib_javatelegramapi_v = 6.2.0 diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala index b1cc8d1..79a5dc9 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -9,9 +9,11 @@ import cc.sukazyo.cono.morny.bot.event.{MornyEventListeners, MornyOnInlineQuery, import cc.sukazyo.cono.morny.bot.query.MornyQueries import cc.sukazyo.cono.morny.util.schedule.Scheduler import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis +import cc.sukazyo.cono.morny.util.time.WatchDog import com.pengrad.telegrambot.TelegramBot import com.pengrad.telegrambot.request.GetMe +import scala.annotation.unused import scala.util.boundary import scala.util.boundary.break @@ -64,10 +66,10 @@ class MornyCoeur (using val config: MornyConfig) { /** [[account]]'s telegram user id */ val userid: Long = __loginResult.userid - /** current Morny's [[MornyTrusted]] instance */ - val trusted: MornyTrusted = MornyTrusted() /** Morny's task [[Scheduler]] */ val tasks: Scheduler = Scheduler() + /** current Morny's [[MornyTrusted]] instance */ + val trusted: MornyTrusted = MornyTrusted() val daemons: MornyDaemons = MornyDaemons() //noinspection ScalaWeakerAccess @@ -80,6 +82,15 @@ class MornyCoeur (using val config: MornyConfig) { eventManager register MornyOnInlineQuery(using queries) //noinspection ScalaUnusedSymbol val events: MornyEventListeners = MornyEventListeners(using eventManager) + eventManager register daemons.reporter.EventStatistics.EventInfoCatcher + @unused + val watchDog: WatchDog = WatchDog("watch-dog", 1000, 1500, { (consumed, _) => + import cc.sukazyo.cono.morny.util.CommonFormat.formatDuration as f + logger warn + s"""Can't keep up! is the server overloaded or host machine fall asleep? + | current tick takes ${f(consumed)} to complete.""".stripMargin + tasks.notifyIt() + }) ///>>> BLOCK START instance configure & startup stage 2 diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java index 2702254..c8ed479 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java @@ -6,6 +6,7 @@ import java.lang.annotation.*; import java.time.ZoneOffset; import java.util.HashSet; import java.util.Set; +import java.util.TimeZone; public class MornyConfig { @@ -109,6 +110,18 @@ public class MornyConfig { */ public final long reportToChat; + /** + * 控制 Morny Coeur 系统的报告的基准时间. + *
+ * 仅会用于 {@link cc.sukazyo.cono.morny.daemon.MornyReport} 内的时间敏感的报告, + * 不会用于 {@code /info} 命令等位置。 + *
+ * 默认使用 {@link TimeZone#getDefault()}.
+ *
+ * @since 1.3.0
+ */
+ @Nonnull public final TimeZone reportZone;
+
/* ======================================= *
* function: dinner query tool *
* ======================================= */
@@ -144,6 +157,7 @@ public class MornyConfig {
this.dinnerTrustedReaders = prototype.dinnerTrustedReaders;
this.dinnerChatId = prototype.dinnerChatId;
this.reportToChat = prototype.reportToChat;
+ this.reportZone = prototype.reportZone;
this.medicationNotifyToChat = prototype.medicationNotifyToChat;
this.medicationTimerUseTimezone = prototype.medicationTimerUseTimezone;
prototype.medicationNotifyAt.forEach(i -> { if (i < 0 || i > 23) throw new CheckFailure.UnavailableTimeInMedicationNotifyAt(); });
@@ -173,6 +187,7 @@ public class MornyConfig {
@Nonnull public final Set${coeur.config.reportZone.getDisplayName}
for following report."""
.stripMargin
).parseMode(ParseMode HTML))
}
@@ -120,4 +131,91 @@ class MornyReport (using coeur: MornyCoeur) {
).parseMode(ParseMode HTML))
}
+ object EventStatistics {
+
+ private var eventTotal = 0
+ private val runningTime: NumericStatistics[DurationMillis] = NumericStatistics()
+
+ def reset (): Unit = {
+ eventTotal = 0; runningTime.reset()
+ }
+
+ private def runningTimeStatisticsHTML: String =
+ runningTime.value match
+ // language=html
+ case None => "<no-statistics>"
+ case Some(value) =>
+ import cc.sukazyo.cono.morny.util.CommonFormat.formatDuration as f
+ s""" - average: ${f(value.total / value.count)}
+ | - max time: ${f(value.max)}
+ | - min time: ${f(value.min)}
+ | - total: ${f(value.total)}
""".stripMargin
+
+ def eventStatisticsHTML: String =
+ import cc.sukazyo.cono.morny.util.UseMath.percentageOf as p
+ val processed = runningTime.count
+ val ignored = eventTotal - processed
+ // language=html
+ s""" - total event received: $eventTotal
+ | - event processed: (${eventTotal p processed}%
) $processed
+ | - event ignored: (${eventTotal p ignored}%
) $ignored
+ | - processed time usage:
+ |${runningTimeStatisticsHTML.indent(3)}""".stripMargin
+
+ object EventInfoCatcher extends EventListener {
+ override def executeFilter (using EventEnv): Boolean = true
+ //noinspection ScalaWeakerAccess
+ case class EventTimeUsed (it: DurationMillis)
+ override def atEventPost (using event: EventEnv): Unit = {
+ eventTotal += 1
+ if event.isEventOk then {
+ val timeUsed = EventTimeUsed(System.currentTimeMillis - event.timeStartup)
+ event provide timeUsed
+ logger debug s"event consumed ${timeUsed.it}ms"
+ runningTime ++ timeUsed.it
+ }
+ }
+ }
+
+ }
+
+ private object DailyReportTask extends CronTask {
+
+ import com.cronutils.model.field.expression.FieldExpressionFactory.*
+
+ override val name: String = "reporter#event"
+ override val cron: Cron = CronBuilder.cron(
+ CronDefinitionBuilder.defineCron
+ .withHours.and
+ .instance
+ ).withHour(on(0)).instance
+ override val zone: ZoneId = coeur.config.reportZone.toZoneId
+
+ //noinspection TypeAnnotation
+ override def main = {
+
+ executeReport(SendMessage(
+ coeur.config.reportToChat,
+ // language=html
+ s"""▌Morny Daily Report
+ |
+ |Event Statistics :
+ |${EventStatistics.eventStatisticsHTML}""".stripMargin
+ ).parseMode(ParseMode.HTML))
+
+ // daily reset
+ EventStatistics.reset()
+
+ }
+
+ }
+
+ def start (): Unit = {
+ coeur.tasks ++ DailyReportTask
+ }
+
+ def stop (): Unit = {
+ coeur.tasks % DailyReportTask
+ }
+
}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala
index 5f11c38..fdbfd1b 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala
@@ -16,4 +16,9 @@ object UseMath {
def ** (other: Int): Double = Math.pow(self, other)
}
+ extension (base: Int) {
+ def percentageOf (another: Int): Int =
+ Math.round((another.toDouble/base)*100).toInt
+ }
+
}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/CronTask.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/CronTask.scala
index 5a8de3d..c6c9607 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/CronTask.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/CronTask.scala
@@ -9,7 +9,7 @@ import scala.jdk.OptionConverters.*
trait CronTask extends RoutineTask {
- private transparent inline def cronCalc = ExecutionTime.forCron(cron)
+ private lazy val cronCalc = ExecutionTime.forCron(cron)
def cron: Cron
diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/RoutineTask.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/RoutineTask.scala
index dce86fe..d7904ab 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/RoutineTask.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/RoutineTask.scala
@@ -13,14 +13,20 @@ import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
*/
trait RoutineTask extends Task {
- private[schedule] var currentScheduledTimeMillis: EpochMillis = firstRoutineTimeMillis
+ private[schedule] var currentScheduledTimeMillis: Option[EpochMillis] = None
/** Next running time of this task.
*
- * Should be auto generated from [[firstRoutineTimeMillis]] and
- * [[nextRoutineTimeMillis]].
+ * Should be auto generated from [[firstRoutineTimeMillis]] when this method
+ * is called at first time, and then from [[nextRoutineTimeMillis]] for following
+ * routines controlled by [[Scheduler]].
*/
- override def scheduledTimeMillis: EpochMillis = currentScheduledTimeMillis
+ override def scheduledTimeMillis: EpochMillis =
+ currentScheduledTimeMillis match
+ case Some(time) => time
+ case None =>
+ currentScheduledTimeMillis = Some(firstRoutineTimeMillis)
+ currentScheduledTimeMillis.get
/** The task scheduled time at initial.
*
diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Scheduler.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Scheduler.scala
index 411be39..4eb066e 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Scheduler.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Scheduler.scala
@@ -63,8 +63,7 @@ class Scheduler {
private val taskList: mutable.TreeSet[Task] = mutable.TreeSet.empty
private var exitAtNextRoutine = false
private var waitForDone = false
- private var currentRunning: Task|Null = _
- private var currentRunning_isScheduledCancel = false
+// private var currentRunning: Task|Null = _
private var runtimeStatus = State.INIT
private val runtime: Thread = new Thread {
@@ -76,20 +75,19 @@ class Scheduler {
if taskList.isEmpty then true
else false
else false
- while (!willExit) {
+ taskList.synchronized { while (!willExit) {
runtimeStatus = State.PREPARE_RUN
- val nextMove: Task|EpochMillis|"None" = taskList.synchronized {
+ val nextMove: Task|EpochMillis|"None" =
taskList.headOption match
case Some(_readyToRun) if System.currentTimeMillis >= _readyToRun.scheduledTimeMillis =>
taskList -= _readyToRun
- currentRunning = _readyToRun
+// currentRunning = _readyToRun
_readyToRun
case Some(_notReady) =>
_notReady.scheduledTimeMillis - System.currentTimeMillis
case None => "None"
- }
nextMove match
case readyToRun: Task =>
@@ -104,31 +102,33 @@ class Scheduler {
runtimeStatus = State.RUNNING_POST
this setName s"${readyToRun.name}#post"
- if currentRunning_isScheduledCancel then {}
+ // this if is used for check if post effect need to be
+ // run. It is useless since the wait/notify changes.
+ if false then {}
else {
- currentRunning match
+ readyToRun match
case routine: RoutineTask =>
- routine.nextRoutineTimeMillis(routine.currentScheduledTimeMillis) match
+ routine.nextRoutineTimeMillis(routine.currentScheduledTimeMillis.get) match
case next: EpochMillis =>
- routine.currentScheduledTimeMillis = next
- if (!currentRunning_isScheduledCancel) schedule(routine)
+ routine.currentScheduledTimeMillis = Some(next)
+ schedule(routine)
case _ =>
case _ =>
}
- currentRunning = null
+// currentRunning = null
this setName runnerName
case needToWaitMillis: EpochMillis =>
runtimeStatus = State.WAITING
- try Thread.sleep(needToWaitMillis)
- catch case _: InterruptedException => {}
+ try taskList.wait(needToWaitMillis)
+ catch case _: (InterruptedException|IllegalArgumentException) => {}
case _: "None" =>
runtimeStatus = State.WAITING_EMPTY
- try Thread.sleep(Long.MaxValue)
+ try taskList.wait()
catch case _: InterruptedException => {}
- }
+ }}
runtimeStatus = State.END
}
@@ -154,9 +154,9 @@ class Scheduler {
* @return [[true]] if the task is added.
*/
def schedule (task: Task): Boolean =
- try taskList.synchronized:
- taskList add task
- finally runtime.interrupt()
+ taskList.synchronized:
+ try taskList add task
+ finally taskList.notifyAll()
/** Remove the task from scheduler task queue.
*
@@ -172,23 +172,16 @@ class Scheduler {
this
/** Remove the task from scheduler task queue.
*
- * If the removal task is running, the current run will be done, but will
- * not do the post effect of the task (like schedule the next routine
- * of [[RoutineTask]]).
+ * If the removal task is running, the method will wait for the current run
+ * complete (and current run post effect complete), then do remove.
*
* @return [[true]] if the task is in task queue or is running, and have been
* succeed removed from task queue.
*/
def cancel (task: Task): Boolean =
- try {
- val succeed = taskList.synchronized { taskList remove task }
- if succeed then succeed
- else if task == currentRunning then
- currentRunning_isScheduledCancel = true
- true
- else false
- }
- finally runtime.interrupt()
+ taskList synchronized:
+ try taskList remove task
+ finally taskList.notifyAll()
/** Count of tasks in the task queue.
*
@@ -205,6 +198,19 @@ class Scheduler {
def runnerState: Thread.State =
runtime.getState
+ /** Manually update the task scheduler.
+ *
+ * If the inner state of the scheduler somehow changed and cannot automatically
+ * update schedule states to schedule the new state, you can call this method
+ * to manually let the task scheduler reschedule it.
+ *
+ * You can also use it with some tick-guard like [[cc.sukazyo.cono.morny.util.time.WatchDog]]
+ * to make the scheduler avoid fails when machine fall asleep or some else conditions.
+ */
+ def notifyIt(): Unit =
+ taskList synchronized:
+ taskList.notifyAll()
+
/** Stop the scheduler's runner, no matter how much task is not run yet.
*
* After call this, it will immediately give a signal to the runner for
@@ -217,8 +223,9 @@ class Scheduler {
* runner is stopped. If you want a sync version, see [[waitForStop]].
*/
def stop (): Unit =
- exitAtNextRoutine = true
- runtime.interrupt()
+ taskList synchronized:
+ exitAtNextRoutine = true
+ taskList.notifyAll()
/** Stop the scheduler's runner, no matter how much task is not run yet,
* and wait for the runner stopped.
@@ -251,8 +258,9 @@ class Scheduler {
*/
//noinspection ScalaWeakerAccess
def tagStopAtAllDone (): Unit =
- waitForDone = true
- runtime.interrupt()
+ taskList synchronized:
+ waitForDone = true
+ taskList.notifyAll()
/** Tag this scheduler runner stop when all of the scheduler's task in task
* queue have been stopped, and wait for the runner stopped.
@@ -264,6 +272,7 @@ class Scheduler {
* thread. The interrupted status of the current
* thread is cleared when this exception is thrown.
*/
+ @throws[InterruptedException]
def waitForStopAtAllDone(): Unit =
tagStopAtAllDone()
runtime.join()
diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/statistics/NumericStatistics.scala b/src/main/scala/cc/sukazyo/cono/morny/util/statistics/NumericStatistics.scala
new file mode 100644
index 0000000..0d1df83
--- /dev/null
+++ b/src/main/scala/cc/sukazyo/cono/morny/util/statistics/NumericStatistics.scala
@@ -0,0 +1,101 @@
+package cc.sukazyo.cono.morny.util.statistics
+
+import scala.annotation.targetName
+
+/** Statistics for numbers.
+ *
+ * Gives a easy way to get amount of numbers min/max/sum value.
+ *
+ * Use [[++]] to collect a value to statistics, use [[value]] to
+ * get the statistic results.
+ *
+ * @param role The [[Numeric]] implementation of the given number type,
+ * required for numeric calculation.
+ * @tparam T The exactly number type
+ */
+class NumericStatistics [T] (using role: Numeric[T]) {
+
+ /** Statistic state values.
+ *
+ * This class instance should only be used in the statistics manager.
+ * You need to converted it to [[State.Immutable]] version when expose
+ * it (use its [[readonly]] method).
+ *
+ * @param total The sum of all data collected.
+ * @param min The minimal value in the collected data.
+ * @param max The maximize value in the collected data.
+ * @param count total collected data count.
+ */
+ class State (
+ var min: T,
+ var max: T,
+ var total: T,
+ var count: Int
+ ) {
+ /** Generate the [[State.Immutable]] readonly copy for this. */
+ def readonly: State.Immutable = State.Immutable(this)
+ }
+ object State:
+ /** The immutable (readonly) version [[State]]. */
+ class Immutable (source: State):
+ /** @see [[State.min]] */
+ val min: T = source.min
+ /** @see [[State.max]] */
+ val max: T = source.max
+ /** @see [[State.total]] */
+ val total: T = source.total
+ /** @see [[State.count]] */
+ val count: Int = source.count
+
+ private var state: Option[State] = None
+
+ /** Collect a new data to the statistic.
+ * @return The [[NumericStatistics]] itself for chained call.
+ */
+ @targetName("collect")
+ def ++ (newOne: T): this.type =
+ state match
+ case Some(current) =>
+ if (role.lt(newOne, current.min)) current.min = newOne
+ if (role.gt(newOne, current.max)) current.max = newOne
+ current.total = role.plus(current.total, newOne)
+ current.count = current.count + 1
+ case None =>
+ state = Some(new State (
+ min = newOne,
+ max = newOne,
+ total = newOne,
+ count = 1
+ ))
+ this
+
+ /** Reset the statistics to the initial state.
+ *
+ * All the collected data will be drop.
+ */
+ def reset (): Unit =
+ state = None
+
+ /** Get the statistic values.
+ *
+ * @return An [[Option]] contains one [[State.Immutable]] object
+ * which refers the statistic state when call this method.
+ * If the statistic have no data recorded, then it will
+ * be [[None]]
+ */
+ def value: Option[State.Immutable] =
+ state match
+ case Some(v) => Some(v.readonly)
+ case None => None
+
+ /** The number counts in the statistics.
+ *
+ * It will always returns a [[Int]] value, regardless if the
+ * statistic is collected some data.
+ */
+ def count: Int =
+ state match
+ case Some(value) => value.count
+ case None => 0
+
+}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/time/WatchDog.scala b/src/main/scala/cc/sukazyo/cono/morny/util/time/WatchDog.scala
new file mode 100644
index 0000000..ffa996e
--- /dev/null
+++ b/src/main/scala/cc/sukazyo/cono/morny/util/time/WatchDog.scala
@@ -0,0 +1,67 @@
+package cc.sukazyo.cono.morny.util.time
+
+import cc.sukazyo.cono.morny.util.EpochDateTime.{DurationMillis, EpochMillis}
+
+trait WatchDog (val isDaemonIt: Boolean = true) extends Thread {
+
+ val threadName: String = "watch-dog"
+ val tickSpeedMillis: DurationMillis = 1000
+ val overloadMillis: DurationMillis = tickSpeedMillis + (tickSpeedMillis/2)
+ private var previousTickTimeMillis: Option[EpochMillis] = None
+
+ this setName threadName
+ this setDaemon isDaemonIt
+
+ this.start()
+
+ override def run(): Unit = {
+ while (!this.isInterrupted) {
+ val currentMillis = System.currentTimeMillis()
+ previousTickTimeMillis match
+ case Some(_previousMillis) =>
+ val consumedMillis = currentMillis - _previousMillis
+ if consumedMillis > overloadMillis then
+ this.overloaded(consumedMillis, consumedMillis - _previousMillis)
+ previousTickTimeMillis = Some(currentMillis)
+ case _ =>
+ previousTickTimeMillis = Some(currentMillis)
+ try Thread.sleep(tickSpeedMillis)
+ catch case _: InterruptedException =>
+ this.interrupt()
+ }
+ }
+
+ def overloaded(consumed: DurationMillis, delayed: DurationMillis): Unit
+
+}
+
+object WatchDog {
+
+ def apply (
+ _threadName: String, _tickSpeedMillis: DurationMillis, _overloadMillis: DurationMillis,
+ overloadedCallback: (DurationMillis, DurationMillis) => Unit
+ ): WatchDog =
+ new WatchDog:
+ override val threadName: String = _threadName
+ override val tickSpeedMillis: DurationMillis = _tickSpeedMillis
+ override val overloadMillis: DurationMillis = _overloadMillis
+ override def overloaded (consumed: DurationMillis, delayed: DurationMillis): Unit = overloadedCallback(consumed, delayed)
+
+ def apply (
+ _threadName: String, _tickSpeedMillis: DurationMillis,
+ overloadedCallback: (DurationMillis, DurationMillis) => Unit
+ ): WatchDog =
+ new WatchDog:
+ override val threadName: String = _threadName
+ override val tickSpeedMillis: DurationMillis = _tickSpeedMillis
+ override def overloaded (consumed: DurationMillis, delayed: DurationMillis): Unit = overloadedCallback(consumed, delayed)
+
+ def apply (
+ _threadName: String,
+ overloadedCallback: (DurationMillis, DurationMillis) => Unit
+ ): WatchDog =
+ new WatchDog:
+ override val threadName: String = _threadName
+ override def overloaded (consumed: DurationMillis, delayed: DurationMillis): Unit = overloadedCallback(consumed, delayed)
+
+}