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:
A.C.Sukazyo Eyre 2023-11-14 13:56:23 +08:00
parent 3d44972233
commit 2c30b5ec09
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
17 changed files with 412 additions and 55 deletions

View File

@ -83,20 +83,25 @@ dependencies {
implementation group: 'cc.sukazyo', name: 'messiva', version: lib_messiva_v implementation group: 'cc.sukazyo', name: 'messiva', version: lib_messiva_v
implementation group: 'cc.sukazyo', name: 'resource-tools', version: lib_resourcetools_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.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('core'), version: lib_sttp_v
implementation group: 'com.softwaremill.sttp.client3', name: scala('okhttp-backend'), 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.google.code.gson', name: 'gson', version: lib_gson_v
implementation group: 'com.cronutils', name: 'cron-utils', version: lib_cron_utils_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'), version: lib_scalatest_v
testImplementation group: 'org.scalatest', name: scala('scalatest-freespec'), 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 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' testRuntimeOnly group: 'com.vladsch.flexmark', name: 'flexmark-all', version: '0.64.6'
} }

View File

@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur
MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono
MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s 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 USE_DELTA = false
VERSION_DELTA = VERSION_DELTA =
@ -19,6 +19,7 @@ lib_scalamodule_xml_v = 2.2.0
lib_messiva_v = 0.2.0 lib_messiva_v = 0.2.0
lib_resourcetools_v = 0.2.2 lib_resourcetools_v = 0.2.2
lib_slf4j_v = 2.0.9
lib_javatelegramapi_v = 6.2.0 lib_javatelegramapi_v = 6.2.0

View File

@ -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.bot.query.MornyQueries
import cc.sukazyo.cono.morny.util.schedule.Scheduler import cc.sukazyo.cono.morny.util.schedule.Scheduler
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis 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.TelegramBot
import com.pengrad.telegrambot.request.GetMe import com.pengrad.telegrambot.request.GetMe
import scala.annotation.unused
import scala.util.boundary import scala.util.boundary
import scala.util.boundary.break import scala.util.boundary.break
@ -64,10 +66,10 @@ class MornyCoeur (using val config: MornyConfig) {
/** [[account]]'s telegram user id */ /** [[account]]'s telegram user id */
val userid: Long = __loginResult.userid val userid: Long = __loginResult.userid
/** current Morny's [[MornyTrusted]] instance */
val trusted: MornyTrusted = MornyTrusted()
/** Morny's task [[Scheduler]] */ /** Morny's task [[Scheduler]] */
val tasks: Scheduler = Scheduler() val tasks: Scheduler = Scheduler()
/** current Morny's [[MornyTrusted]] instance */
val trusted: MornyTrusted = MornyTrusted()
val daemons: MornyDaemons = MornyDaemons() val daemons: MornyDaemons = MornyDaemons()
//noinspection ScalaWeakerAccess //noinspection ScalaWeakerAccess
@ -80,6 +82,15 @@ class MornyCoeur (using val config: MornyConfig) {
eventManager register MornyOnInlineQuery(using queries) eventManager register MornyOnInlineQuery(using queries)
//noinspection ScalaUnusedSymbol //noinspection ScalaUnusedSymbol
val events: MornyEventListeners = MornyEventListeners(using eventManager) 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 ///>>> BLOCK START instance configure & startup stage 2

View File

@ -6,6 +6,7 @@ import java.lang.annotation.*;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.TimeZone;
public class MornyConfig { public class MornyConfig {
@ -109,6 +110,18 @@ public class MornyConfig {
*/ */
public final long reportToChat; 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 * * function: dinner query tool *
* ======================================= */ * ======================================= */
@ -144,6 +157,7 @@ public class MornyConfig {
this.dinnerTrustedReaders = prototype.dinnerTrustedReaders; this.dinnerTrustedReaders = prototype.dinnerTrustedReaders;
this.dinnerChatId = prototype.dinnerChatId; this.dinnerChatId = prototype.dinnerChatId;
this.reportToChat = prototype.reportToChat; this.reportToChat = prototype.reportToChat;
this.reportZone = prototype.reportZone;
this.medicationNotifyToChat = prototype.medicationNotifyToChat; this.medicationNotifyToChat = prototype.medicationNotifyToChat;
this.medicationTimerUseTimezone = prototype.medicationTimerUseTimezone; this.medicationTimerUseTimezone = prototype.medicationTimerUseTimezone;
prototype.medicationNotifyAt.forEach(i -> { if (i < 0 || i > 23) throw new CheckFailure.UnavailableTimeInMedicationNotifyAt(); }); 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<>(); @Nonnull public final Set<Long> dinnerTrustedReaders = new HashSet<>();
public long dinnerChatId = -1L; public long dinnerChatId = -1L;
public long reportToChat = -1L; public long reportToChat = -1L;
@Nonnull public TimeZone reportZone = TimeZone.getDefault();
public long medicationNotifyToChat = -1L; public long medicationNotifyToChat = -1L;
@Nonnull public ZoneOffset medicationTimerUseTimezone = ZoneOffset.UTC; @Nonnull public ZoneOffset medicationTimerUseTimezone = ZoneOffset.UTC;
@Nonnull public final Set<Integer> medicationNotifyAt = new HashSet<>(); @Nonnull public final Set<Integer> medicationNotifyAt = new HashSet<>();

View File

@ -51,6 +51,7 @@ object ServerMain {
case "--master" | "-mm" => i+=1 ; config.trustedMaster = args(i)toLong case "--master" | "-mm" => i+=1 ; config.trustedMaster = args(i)toLong
case "--trusted-chat" | "-trs" => i+=1 ; config.trustedChat = 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-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 "--trusted-reader-dinner" | "-trsd" => i+=1 ; config.dinnerTrustedReaders add (args(i)toLong)
case "--dinner-chat" | "-chd" => i+=1 ; config.dinnerChatId = args(i)toLong case "--dinner-chat" | "-chd" => i+=1 ; config.dinnerChatId = args(i)toLong

View File

@ -1,5 +1,6 @@
package cc.sukazyo.cono.morny.bot.api package cc.sukazyo.cono.morny.bot.api
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.Update
import scala.collection.mutable import scala.collection.mutable
@ -12,6 +13,7 @@ class EventEnv (
private var _isOk: Int = 0 private var _isOk: Int = 0
private val variables: mutable.HashMap[Class[?], Any] = mutable.HashMap.empty private val variables: mutable.HashMap[Class[?], Any] = mutable.HashMap.empty
val timeStartup: EpochMillis = System.currentTimeMillis
def isEventOk: Boolean = _isOk > 0 def isEventOk: Boolean = _isOk > 0

View File

@ -2,6 +2,25 @@ package cc.sukazyo.cono.morny.bot.api
trait EventListener () { 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 onMessage (using EventEnv): Unit = {}
def onEditedMessage (using EventEnv): Unit = {} def onEditedMessage (using EventEnv): Unit = {}
def onChannelPost (using EventEnv): Unit = {} def onChannelPost (using EventEnv): Unit = {}

View File

@ -32,7 +32,9 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
override def run (): Unit = { override def run (): Unit = {
given env: EventEnv = EventEnv(update) given env: EventEnv = EventEnv(update)
boundary { for (i <- listeners) { boundary { for (i <- listeners) {
try {
if (i.executeFilter) try {
updateThreadName("message") updateThreadName("message")
if update.message ne null then i.onMessage if update.message ne null then i.onMessage
updateThreadName("edited-message") updateThreadName("edited-message")
@ -61,6 +63,10 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
if update.chatMember ne null then i.onChatMemberUpdated if update.chatMember ne null then i.onChatMemberUpdated
updateThreadName("chat-join-request") updateThreadName("chat-join-request")
if update.chatJoinRequest ne null then i.onChatJoinRequest if update.chatJoinRequest ne null then i.onChatJoinRequest
updateThreadName("#post")
i.atEventPost
} catch case e => { } catch case e => {
val errorMessage = StringBuilder() val errorMessage = StringBuilder()
errorMessage ++= "Event throws unexpected exception:\n" errorMessage ++= "Event throws unexpected exception:\n"
@ -75,7 +81,7 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
logger error errorMessage.toString logger error errorMessage.toString
coeur.daemons.reporter.exception(e, "on event running") coeur.daemons.reporter.exception(e, "on event running")
} }
if env.isEventOk then boundary.break()
}} }}
} }

View File

@ -23,11 +23,12 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
val VERSION = "version" val VERSION = "version"
val VERSION_2 = "v" val VERSION_2 = "v"
val TASKS = "tasks" val TASKS = "tasks"
val EVENTS = "event"
} }
override val name: String = "info" override val name: String = "info"
override val aliases: Array[ICommandAlias]|Null = null 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 val description: String = "输出当前 Morny 的各种信息"
override def execute (using command: InputCommand, event: Update): Unit = { 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.RUNTIME => echoRuntime
case Subs.VERSION | Subs.VERSION_2 => echoVersion case Subs.VERSION | Subs.VERSION_2 => echoVersion
case Subs.TASKS => echoTasksStatus case Subs.TASKS => echoTasksStatus
case Subs.EVENTS => echoEventStatistics
case _ => echo404 case _ => echo404
} }
@ -159,6 +161,16 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
).parseMode(ParseMode.HTML).replyToMessageId(update.message.messageId) ).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 = private def echo404 (using event: Update): Unit =
coeur.account exec new SendSticker( coeur.account exec new SendSticker(
event.message.chat.id, event.message.chat.id,

View File

@ -13,8 +13,8 @@ class MornyDaemons (using val coeur: MornyCoeur) {
logger notice "ALL Morny Daemons starting..." logger notice "ALL Morny Daemons starting..."
// TrackerDataManager.init();
medicationTimer.start() medicationTimer.start()
reporter.start()
logger notice "Morny Daemons started." logger notice "Morny Daemons started."
@ -24,9 +24,8 @@ class MornyDaemons (using val coeur: MornyCoeur) {
logger notice "stopping All Morny Daemons..." logger notice "stopping All Morny Daemons..."
// TrackerDataManager.DAEMON.interrupt();
medicationTimer.stop() medicationTimer.stop()
// TrackerDataManager.trackingLock.lock(); reporter.stop()
logger notice "stopped ALL Morny Daemons." logger notice "stopped ALL Morny Daemons."
} }

View File

@ -2,17 +2,26 @@ package cc.sukazyo.cono.morny.daemon
import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig} import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig}
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} 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.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.event.EventRuntimeException
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* 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.formatting.TelegramParseEscape.escapeHtml as h
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec 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.google.gson.GsonBuilder
import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.model.User import com.pengrad.telegrambot.model.User
import com.pengrad.telegrambot.request.{BaseRequest, SendMessage} import com.pengrad.telegrambot.request.{BaseRequest, SendMessage}
import com.pengrad.telegrambot.response.BaseResponse import com.pengrad.telegrambot.response.BaseResponse
import java.time.ZoneId
class MornyReport (using coeur: MornyCoeur) { class MornyReport (using coeur: MornyCoeur) {
private val enabled = coeur.config.reportToChat != -1 private val enabled = coeur.config.reportToChat != -1
@ -67,10 +76,12 @@ class MornyReport (using coeur: MornyCoeur) {
// language=html // language=html
s"""<b>▌Morny Logged in</b> s"""<b>▌Morny Logged in</b>
|-v $getVersionAllFullTagHTML |-v $getVersionAllFullTagHTML
|as user @${coeur.username} |Logged into user: @${coeur.username}
| |
|as config fields: |as config fields:
|${sectionConfigFields(coeur.config)}""" |${sectionConfigFields(coeur.config)}
|
|Report Daemon will use TimeZone <code>${coeur.config.reportZone.getDisplayName}</code> for following report."""
.stripMargin .stripMargin
).parseMode(ParseMode HTML)) ).parseMode(ParseMode HTML))
} }
@ -120,4 +131,91 @@ class MornyReport (using coeur: MornyCoeur) {
).parseMode(ParseMode HTML)) ).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>&lt;no-statistics&gt;</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
}
} }

View File

@ -16,4 +16,9 @@ object UseMath {
def ** (other: Int): Double = Math.pow(self, other) def ** (other: Int): Double = Math.pow(self, other)
} }
extension (base: Int) {
def percentageOf (another: Int): Int =
Math.round((another.toDouble/base)*100).toInt
}
} }

View File

@ -9,7 +9,7 @@ import scala.jdk.OptionConverters.*
trait CronTask extends RoutineTask { trait CronTask extends RoutineTask {
private transparent inline def cronCalc = ExecutionTime.forCron(cron) private lazy val cronCalc = ExecutionTime.forCron(cron)
def cron: Cron def cron: Cron

View File

@ -13,14 +13,20 @@ import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
*/ */
trait RoutineTask extends Task { trait RoutineTask extends Task {
private[schedule] var currentScheduledTimeMillis: EpochMillis = firstRoutineTimeMillis private[schedule] var currentScheduledTimeMillis: Option[EpochMillis] = None
/** Next running time of this task. /** Next running time of this task.
* *
* Should be auto generated from [[firstRoutineTimeMillis]] and * Should be auto generated from [[firstRoutineTimeMillis]] when this method
* [[nextRoutineTimeMillis]]. * 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. /** The task scheduled time at initial.
* *

View File

@ -63,8 +63,7 @@ class Scheduler {
private val taskList: mutable.TreeSet[Task] = mutable.TreeSet.empty private val taskList: mutable.TreeSet[Task] = mutable.TreeSet.empty
private var exitAtNextRoutine = false private var exitAtNextRoutine = false
private var waitForDone = false private var waitForDone = false
private var currentRunning: Task|Null = _ // private var currentRunning: Task|Null = _
private var currentRunning_isScheduledCancel = false
private var runtimeStatus = State.INIT private var runtimeStatus = State.INIT
private val runtime: Thread = new Thread { private val runtime: Thread = new Thread {
@ -76,20 +75,19 @@ class Scheduler {
if taskList.isEmpty then true if taskList.isEmpty then true
else false else false
else false else false
while (!willExit) { taskList.synchronized { while (!willExit) {
runtimeStatus = State.PREPARE_RUN runtimeStatus = State.PREPARE_RUN
val nextMove: Task|EpochMillis|"None" = taskList.synchronized { val nextMove: Task|EpochMillis|"None" =
taskList.headOption match taskList.headOption match
case Some(_readyToRun) if System.currentTimeMillis >= _readyToRun.scheduledTimeMillis => case Some(_readyToRun) if System.currentTimeMillis >= _readyToRun.scheduledTimeMillis =>
taskList -= _readyToRun taskList -= _readyToRun
currentRunning = _readyToRun // currentRunning = _readyToRun
_readyToRun _readyToRun
case Some(_notReady) => case Some(_notReady) =>
_notReady.scheduledTimeMillis - System.currentTimeMillis _notReady.scheduledTimeMillis - System.currentTimeMillis
case None => "None" case None => "None"
}
nextMove match nextMove match
case readyToRun: Task => case readyToRun: Task =>
@ -104,31 +102,33 @@ class Scheduler {
runtimeStatus = State.RUNNING_POST runtimeStatus = State.RUNNING_POST
this setName s"${readyToRun.name}#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 { else {
currentRunning match readyToRun match
case routine: RoutineTask => case routine: RoutineTask =>
routine.nextRoutineTimeMillis(routine.currentScheduledTimeMillis) match routine.nextRoutineTimeMillis(routine.currentScheduledTimeMillis.get) match
case next: EpochMillis => case next: EpochMillis =>
routine.currentScheduledTimeMillis = next routine.currentScheduledTimeMillis = Some(next)
if (!currentRunning_isScheduledCancel) schedule(routine) schedule(routine)
case _ => case _ =>
case _ => case _ =>
} }
currentRunning = null // currentRunning = null
this setName runnerName this setName runnerName
case needToWaitMillis: EpochMillis => case needToWaitMillis: EpochMillis =>
runtimeStatus = State.WAITING runtimeStatus = State.WAITING
try Thread.sleep(needToWaitMillis) try taskList.wait(needToWaitMillis)
catch case _: InterruptedException => {} catch case _: (InterruptedException|IllegalArgumentException) => {}
case _: "None" => case _: "None" =>
runtimeStatus = State.WAITING_EMPTY runtimeStatus = State.WAITING_EMPTY
try Thread.sleep(Long.MaxValue) try taskList.wait()
catch case _: InterruptedException => {} catch case _: InterruptedException => {}
} }}
runtimeStatus = State.END runtimeStatus = State.END
} }
@ -154,9 +154,9 @@ class Scheduler {
* @return [[true]] if the task is added. * @return [[true]] if the task is added.
*/ */
def schedule (task: Task): Boolean = def schedule (task: Task): Boolean =
try taskList.synchronized: taskList.synchronized:
taskList add task try taskList add task
finally runtime.interrupt() finally taskList.notifyAll()
/** Remove the task from scheduler task queue. /** Remove the task from scheduler task queue.
* *
@ -172,23 +172,16 @@ class Scheduler {
this this
/** Remove the task from scheduler task queue. /** Remove the task from scheduler task queue.
* *
* If the removal task is running, the current run will be done, but will * If the removal task is running, the method will wait for the current run
* not do the post effect of the task (like schedule the next routine * complete (and current run post effect complete), then do remove.
* of [[RoutineTask]]).
* *
* @return [[true]] if the task is in task queue or is running, and have been * @return [[true]] if the task is in task queue or is running, and have been
* succeed removed from task queue. * succeed removed from task queue.
*/ */
def cancel (task: Task): Boolean = def cancel (task: Task): Boolean =
try { taskList synchronized:
val succeed = taskList.synchronized { taskList remove task } try taskList remove task
if succeed then succeed finally taskList.notifyAll()
else if task == currentRunning then
currentRunning_isScheduledCancel = true
true
else false
}
finally runtime.interrupt()
/** Count of tasks in the task queue. /** Count of tasks in the task queue.
* *
@ -205,6 +198,19 @@ class Scheduler {
def runnerState: Thread.State = def runnerState: Thread.State =
runtime.getState 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. /** 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 * 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]]. * runner is stopped. If you want a sync version, see [[waitForStop]].
*/ */
def stop (): Unit = def stop (): Unit =
taskList synchronized:
exitAtNextRoutine = true exitAtNextRoutine = true
runtime.interrupt() taskList.notifyAll()
/** Stop the scheduler's runner, no matter how much task is not run yet, /** Stop the scheduler's runner, no matter how much task is not run yet,
* and wait for the runner stopped. * and wait for the runner stopped.
@ -251,8 +258,9 @@ class Scheduler {
*/ */
//noinspection ScalaWeakerAccess //noinspection ScalaWeakerAccess
def tagStopAtAllDone (): Unit = def tagStopAtAllDone (): Unit =
taskList synchronized:
waitForDone = true waitForDone = true
runtime.interrupt() taskList.notifyAll()
/** Tag this scheduler runner stop when all of the scheduler's task in task /** Tag this scheduler runner stop when all of the scheduler's task in task
* queue have been stopped, and wait for the runner stopped. * queue have been stopped, and wait for the runner stopped.
@ -264,6 +272,7 @@ class Scheduler {
* thread. The interrupted status of the current * thread. The interrupted status of the current
* thread is cleared when this exception is thrown. * thread is cleared when this exception is thrown.
*/ */
@throws[InterruptedException]
def waitForStopAtAllDone(): Unit = def waitForStopAtAllDone(): Unit =
tagStopAtAllDone() tagStopAtAllDone()
runtime.join() runtime.join()

View File

@ -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
}

View File

@ -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)
}