mirror of
https://github.com/Eyre-S/Coeur-Morny-Cono.git
synced 2024-11-22 11:14:55 +08:00
add event statistics, fix CronTask
- add for EventEnv a timeStartup field - cha EventListener and EventListenerManager - add for EventListener a method executeFilter used to manager if an event should be run. This replaced the condition statement inside the EventListenerManager - add for EventListener a method atEventPost, this will run at current event listener is on complete - add for MornyConfig a reportZone field - can be set by `--report-zone` - used for controlling Morny Report daemon uses the zoned time to send report. default is system default time zone. - add for MornyReport new EventStatistics and DailyReportTask - add for MornyInformation command new subcommand `event` to manually show MornyReport.EventStatistics info. - add WatchDog and MornyCoeur.watchDog, used for checking if the machine is in sleep mode and notify the MornyCoeur.tasks to avoid timing problem - fix CronTask frequency got initialize problem - add slf4j-nop for project
This commit is contained in:
parent
3d44972233
commit
2c30b5ec09
13
build.gradle
13
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'
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 系统的报告的基准时间.
|
||||
* <p>
|
||||
* 仅会用于 {@link cc.sukazyo.cono.morny.daemon.MornyReport} 内的时间敏感的报告,
|
||||
* 不会用于 {@code /info} 命令等位置。
|
||||
* <p>
|
||||
* 默认使用 {@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<Long> dinnerTrustedReaders = new HashSet<>();
|
||||
public long dinnerChatId = -1L;
|
||||
public long reportToChat = -1L;
|
||||
@Nonnull public TimeZone reportZone = TimeZone.getDefault();
|
||||
public long medicationNotifyToChat = -1L;
|
||||
@Nonnull public ZoneOffset medicationTimerUseTimezone = ZoneOffset.UTC;
|
||||
@Nonnull public final Set<Integer> medicationNotifyAt = new HashSet<>();
|
||||
|
@ -51,6 +51,7 @@ object ServerMain {
|
||||
case "--master" | "-mm" => i+=1 ; config.trustedMaster = args(i)toLong
|
||||
case "--trusted-chat" | "-trs" => i+=1 ; config.trustedChat = args(i)toLong
|
||||
case "--report-to" => i+=1; config.reportToChat = args(i)toLong
|
||||
case "--report-zone" => i+=1; config.reportZone = TimeZone.getTimeZone(args(i))
|
||||
|
||||
case "--trusted-reader-dinner" | "-trsd" => i+=1 ; config.dinnerTrustedReaders add (args(i)toLong)
|
||||
case "--dinner-chat" | "-chd" => i+=1 ; config.dinnerChatId = args(i)toLong
|
||||
|
@ -1,5 +1,6 @@
|
||||
package cc.sukazyo.cono.morny.bot.api
|
||||
|
||||
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
|
||||
import scala.collection.mutable
|
||||
@ -12,6 +13,7 @@ class EventEnv (
|
||||
|
||||
private var _isOk: Int = 0
|
||||
private val variables: mutable.HashMap[Class[?], Any] = mutable.HashMap.empty
|
||||
val timeStartup: EpochMillis = System.currentTimeMillis
|
||||
|
||||
def isEventOk: Boolean = _isOk > 0
|
||||
|
||||
|
@ -2,6 +2,25 @@ package cc.sukazyo.cono.morny.bot.api
|
||||
|
||||
trait EventListener () {
|
||||
|
||||
/** Determine if this event listener should be processed.
|
||||
*
|
||||
* Default implementation is it only be [[true]] when the event
|
||||
* is not ok yet (when [[EventEnv.isEventOk]] is false).
|
||||
*
|
||||
* Notice that: You should not override this method to filter some
|
||||
* affair level conditions (such as if this update contains a text
|
||||
* message), you should write them to the listener function! This
|
||||
* method is just for event low-level controls.
|
||||
*
|
||||
* @param env The [[EventEnv event variable]].
|
||||
* @return [[true]] if this event listener should run; [[false]]
|
||||
* if it should not run.
|
||||
*/
|
||||
def executeFilter (using env: EventEnv): Boolean =
|
||||
if env.isEventOk then false else true
|
||||
|
||||
def atEventPost (using EventEnv): Unit = {}
|
||||
|
||||
def onMessage (using EventEnv): Unit = {}
|
||||
def onEditedMessage (using EventEnv): Unit = {}
|
||||
def onChannelPost (using EventEnv): Unit = {}
|
||||
|
@ -32,7 +32,9 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
|
||||
override def run (): Unit = {
|
||||
given env: EventEnv = EventEnv(update)
|
||||
boundary { for (i <- listeners) {
|
||||
try {
|
||||
|
||||
if (i.executeFilter) try {
|
||||
|
||||
updateThreadName("message")
|
||||
if update.message ne null then i.onMessage
|
||||
updateThreadName("edited-message")
|
||||
@ -61,6 +63,10 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
|
||||
if update.chatMember ne null then i.onChatMemberUpdated
|
||||
updateThreadName("chat-join-request")
|
||||
if update.chatJoinRequest ne null then i.onChatJoinRequest
|
||||
|
||||
updateThreadName("#post")
|
||||
i.atEventPost
|
||||
|
||||
} catch case e => {
|
||||
val errorMessage = StringBuilder()
|
||||
errorMessage ++= "Event throws unexpected exception:\n"
|
||||
@ -75,7 +81,7 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
|
||||
logger error errorMessage.toString
|
||||
coeur.daemons.reporter.exception(e, "on event running")
|
||||
}
|
||||
if env.isEventOk then boundary.break()
|
||||
|
||||
}}
|
||||
}
|
||||
|
||||
|
@ -23,11 +23,12 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
|
||||
val VERSION = "version"
|
||||
val VERSION_2 = "v"
|
||||
val TASKS = "tasks"
|
||||
val EVENTS = "event"
|
||||
}
|
||||
|
||||
override val name: String = "info"
|
||||
override val aliases: Array[ICommandAlias]|Null = null
|
||||
override val paramRule: String = "[(version|runtime|stickers[.IDs]|tasks)]"
|
||||
override val paramRule: String = "[(version|runtime|stickers[.IDs]|tasks|event)]"
|
||||
override val description: String = "输出当前 Morny 的各种信息"
|
||||
|
||||
override def execute (using command: InputCommand, event: Update): Unit = {
|
||||
@ -44,6 +45,7 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
|
||||
case Subs.RUNTIME => echoRuntime
|
||||
case Subs.VERSION | Subs.VERSION_2 => echoVersion
|
||||
case Subs.TASKS => echoTasksStatus
|
||||
case Subs.EVENTS => echoEventStatistics
|
||||
case _ => echo404
|
||||
}
|
||||
|
||||
@ -159,6 +161,16 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
|
||||
).parseMode(ParseMode.HTML).replyToMessageId(update.message.messageId)
|
||||
}
|
||||
|
||||
private def echoEventStatistics (using update: Update): Unit = {
|
||||
coeur.account exec SendMessage(
|
||||
update.message.chat.id,
|
||||
// language=html
|
||||
s"""<b>Event Statistics :</b>
|
||||
|in today
|
||||
|${coeur.daemons.reporter.EventStatistics.eventStatisticsHTML}""".stripMargin
|
||||
).parseMode(ParseMode.HTML).replyToMessageId(update.message.messageId)
|
||||
}
|
||||
|
||||
private def echo404 (using event: Update): Unit =
|
||||
coeur.account exec new SendSticker(
|
||||
event.message.chat.id,
|
||||
|
@ -13,8 +13,8 @@ class MornyDaemons (using val coeur: MornyCoeur) {
|
||||
|
||||
logger notice "ALL Morny Daemons starting..."
|
||||
|
||||
// TrackerDataManager.init();
|
||||
medicationTimer.start()
|
||||
reporter.start()
|
||||
|
||||
logger notice "Morny Daemons started."
|
||||
|
||||
@ -24,9 +24,8 @@ class MornyDaemons (using val coeur: MornyCoeur) {
|
||||
|
||||
logger notice "stopping All Morny Daemons..."
|
||||
|
||||
// TrackerDataManager.DAEMON.interrupt();
|
||||
medicationTimer.stop()
|
||||
// TrackerDataManager.trackingLock.lock();
|
||||
reporter.stop()
|
||||
|
||||
logger notice "stopped ALL Morny Daemons."
|
||||
}
|
||||
|
@ -2,17 +2,26 @@ package cc.sukazyo.cono.morny.daemon
|
||||
|
||||
import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig}
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
|
||||
import cc.sukazyo.cono.morny.data.MornyInformation.getVersionAllFullTagHTML
|
||||
import cc.sukazyo.cono.morny.util.statistics.NumericStatistics
|
||||
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
|
||||
import cc.sukazyo.cono.morny.util.EpochDateTime.DurationMillis
|
||||
import cc.sukazyo.cono.morny.util.schedule.CronTask
|
||||
import com.cronutils.builder.CronBuilder
|
||||
import com.cronutils.model.Cron
|
||||
import com.cronutils.model.definition.CronDefinitionBuilder
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.model.User
|
||||
import com.pengrad.telegrambot.request.{BaseRequest, SendMessage}
|
||||
import com.pengrad.telegrambot.response.BaseResponse
|
||||
|
||||
import java.time.ZoneId
|
||||
|
||||
class MornyReport (using coeur: MornyCoeur) {
|
||||
|
||||
private val enabled = coeur.config.reportToChat != -1
|
||||
@ -67,10 +76,12 @@ class MornyReport (using coeur: MornyCoeur) {
|
||||
// language=html
|
||||
s"""<b>▌Morny Logged in</b>
|
||||
|-v $getVersionAllFullTagHTML
|
||||
|as user @${coeur.username}
|
||||
|Logged into user: @${coeur.username}
|
||||
|
|
||||
|as config fields:
|
||||
|${sectionConfigFields(coeur.config)}"""
|
||||
|${sectionConfigFields(coeur.config)}
|
||||
|
|
||||
|Report Daemon will use TimeZone <code>${coeur.config.reportZone.getDisplayName}</code> 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 => "<i><u><no-statistics></u></i>"
|
||||
case Some(value) =>
|
||||
import cc.sukazyo.cono.morny.util.CommonFormat.formatDuration as f
|
||||
s""" - <i>average</i>: <code>${f(value.total / value.count)}</code>
|
||||
| - <i>max time</i>: <code>${f(value.max)}</code>
|
||||
| - <i>min time</i>: <code>${f(value.min)}</code>
|
||||
| - <i>total</i>: <code>${f(value.total)}</code>""".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""" - <i>total event received</i>: <code>$eventTotal</code>
|
||||
| - <i>event processed</i>: (<code>${eventTotal p processed}%</code>) <code>$processed</code>
|
||||
| - <i>event ignored</i>: (<code>${eventTotal p ignored}%</code>) <code>$ignored</code>
|
||||
| - <i>processed time usage</i>:
|
||||
|${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
|
||||
|
|
||||
|<b>Event Statistics :</b>
|
||||
|${EventStatistics.eventStatisticsHTML}""".stripMargin
|
||||
).parseMode(ParseMode.HTML))
|
||||
|
||||
// daily reset
|
||||
EventStatistics.reset()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def start (): Unit = {
|
||||
coeur.tasks ++ DailyReportTask
|
||||
}
|
||||
|
||||
def stop (): Unit = {
|
||||
coeur.tasks % DailyReportTask
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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 =
|
||||
taskList synchronized:
|
||||
exitAtNextRoutine = true
|
||||
runtime.interrupt()
|
||||
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 =
|
||||
taskList synchronized:
|
||||
waitForDone = true
|
||||
runtime.interrupt()
|
||||
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()
|
||||
|
@ -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
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user