for event, fix wrong OK stats, add CANCELED tag

- Now the status of EventEnv is a State array that infers the state history
  - State can be OK or CANCELED, and can be set multiple times
  - state method can get the last state set, and status method can get the state history
  - Default EventListener.executeFilter implementation is changed to true if stats is null
- add consume[T](T=>Unit) for EventEnv, to simplifying the old consume[T](Class[T])(T=>Unit)
- changed execution sort of EventListener in EventListenerManager. Now atEventPost method will be run after all events' normal listeners complete.
- cha OnMedicationNotifyApply will only tag event as OK when the refresh function works (fixed part of the wrong OK state)
- cha MornyOnUpdateTimestampOffsetLock tag event CANCELED but not OK to fix part of the wrong OK state
This commit is contained in:
A.C.Sukazyo Eyre 2023-11-20 11:18:32 +08:00
parent 7ee4a0d3c5
commit c5c6683459
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
10 changed files with 131 additions and 79 deletions

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-dev5 VERSION = 1.3.0-dev6
USE_DELTA = false USE_DELTA = false
VERSION_DELTA = VERSION_DELTA =

View File

@ -1,9 +1,11 @@
package cc.sukazyo.cono.morny.bot.api package cc.sukazyo.cono.morny.bot.api
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
import cc.sukazyo.messiva.utils.StackUtils
import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.Update
import scala.collection.mutable import scala.collection.mutable
import scala.reflect.{classTag, ClassTag}
class EventEnv ( class EventEnv (
@ -11,15 +13,34 @@ class EventEnv (
) { ) {
private var _isOk: Int = 0 trait StateSource (val from: StackTraceElement)
enum State:
case OK (_from: StackTraceElement) extends State with StateSource(_from)
case CANCELED (_from: StackTraceElement) extends State with StateSource(_from)
private val _status: mutable.ListBuffer[State] = mutable.ListBuffer.empty
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 val timeStartup: EpochMillis = System.currentTimeMillis
def isEventOk: Boolean = _isOk > 0 def isEventOk: Boolean = _status.lastOption match
case Some(x) if x == State.OK => true
case _ => false
//noinspection UnitMethodIsParameterless //noinspection UnitMethodIsParameterless
def setEventOk: Unit = def setEventOk: Unit =
_isOk = _isOk + 1 _status += State.OK(StackUtils.getStackTrace(1)(1))
//noinspection UnitMethodIsParameterless
def setEventCanceled: Unit =
_status += State.CANCELED(StackUtils.getStackTrace(1)(1))
def state: State|Null =
_status.lastOption match
case Some(x) => x
case None => null
def status: List[State] =
_status.toList
def provide (i: Any): Unit = def provide (i: Any): Unit =
variables += (i.getClass -> i) variables += (i.getClass -> i)
@ -30,6 +51,11 @@ class EventEnv (
case None => ConsumeResult(false) case None => ConsumeResult(false)
} }
def consume [T: ClassTag] (consumer: T => Unit): ConsumeResult =
variables get classTag[T].runtimeClass match
case Some(i) => consumer(i.asInstanceOf[T]); ConsumeResult(true)
case None => ConsumeResult(false)
class ConsumeResult (success: Boolean) { class ConsumeResult (success: Boolean) {
def onfail (processor: => Unit): Unit = { def onfail (processor: => Unit): Unit = {
if !success then processor if !success then processor

View File

@ -17,8 +17,15 @@ trait EventListener () {
* if it should not run. * if it should not run.
*/ */
def executeFilter (using env: EventEnv): Boolean = def executeFilter (using env: EventEnv): Boolean =
if env.isEventOk then false else true if env.state == null then true else false
/** Run at all event listeners' listen methods done.
*
* Listen methods is the methods defined in [[EventListener this]]
* trait starts with `on`.
*
* This method will always run no matter the result of [[executeFilter]]
*/
def atEventPost (using EventEnv): Unit = {} def atEventPost (using EventEnv): Unit = {}
def onMessage (using EventEnv): Unit = {} def onMessage (using EventEnv): Unit = {}

View File

@ -9,7 +9,6 @@ import com.pengrad.telegrambot.UpdatesListener
import scala.collection.mutable import scala.collection.mutable
import scala.language.postfixOps import scala.language.postfixOps
import scala.util.boundary
/** Contains a [[mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]]. /** Contains a [[mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]].
* *
@ -30,59 +29,66 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
this setName s"upd-${update.updateId()}-$t" this setName s"upd-${update.updateId()}-$t"
override def run (): Unit = { override def run (): Unit = {
given env: EventEnv = EventEnv(update) given env: EventEnv = EventEnv(update)
boundary { for (i <- listeners) {
if (i.executeFilter) try { for (i <- listeners)
if (i.executeFilter)
runEventListener(i)
for (i <- listeners)
runEventPost(i)
updateThreadName("message") }
if update.message ne null then i.onMessage
updateThreadName("edited-message")
if update.editedMessage ne null then i.onEditedMessage
updateThreadName("channel-post")
if update.channelPost ne null then i.onChannelPost
updateThreadName("edited-channel-post")
if update.editedChannelPost ne null then i.onEditedChannelPost
updateThreadName("inline-query")
if update.inlineQuery ne null then i.onInlineQuery
updateThreadName("chosen-inline-result")
if update.chosenInlineResult ne null then i.onChosenInlineResult
updateThreadName("callback-query")
if update.callbackQuery ne null then i.onCallbackQuery
updateThreadName("shipping-query")
if update.shippingQuery ne null then i.onShippingQuery
updateThreadName("pre-checkout-query")
if update.preCheckoutQuery ne null then i.onPreCheckoutQuery
updateThreadName("poll")
if update.poll ne null then i.onPoll
updateThreadName("poll-answer")
if update.pollAnswer ne null then i.onPollAnswer
updateThreadName("my-chat-member")
if update.myChatMember ne null then i.onMyChatMemberUpdated
updateThreadName("chat-member")
if update.chatMember ne null then i.onChatMemberUpdated
updateThreadName("chat-join-request")
if update.chatJoinRequest ne null then i.onChatJoinRequest
updateThreadName("#post") private def runEventPost (i: EventListener)(using EventEnv): Unit = {
i.atEventPost updateThreadName("#post")
i.atEventPost
}
} catch case e => { private def runEventListener (i: EventListener)(using EventEnv): Unit = {
val errorMessage = StringBuilder() try {
errorMessage ++= "Event throws unexpected exception:\n" updateThreadName("message")
errorMessage ++= (exceptionLog(e) indent 4) if update.message ne null then i.onMessage
e match updateThreadName("edited-message")
case actionFailed: EventRuntimeException.ActionFailed => if update.editedMessage ne null then i.onEditedMessage
errorMessage ++= "\ntg-api action: response track: " updateThreadName("channel-post")
errorMessage ++= (GsonBuilder().setPrettyPrinting().create().toJson( if update.channelPost ne null then i.onChannelPost
actionFailed.response updateThreadName("edited-channel-post")
) indent 4) ++= "\n" if update.editedChannelPost ne null then i.onEditedChannelPost
case _ => updateThreadName("inline-query")
logger error errorMessage.toString if update.inlineQuery ne null then i.onInlineQuery
coeur.daemons.reporter.exception(e, "on event running") updateThreadName("chosen-inline-result")
} if update.chosenInlineResult ne null then i.onChosenInlineResult
updateThreadName("callback-query")
}} if update.callbackQuery ne null then i.onCallbackQuery
updateThreadName("shipping-query")
if update.shippingQuery ne null then i.onShippingQuery
updateThreadName("pre-checkout-query")
if update.preCheckoutQuery ne null then i.onPreCheckoutQuery
updateThreadName("poll")
if update.poll ne null then i.onPoll
updateThreadName("poll-answer")
if update.pollAnswer ne null then i.onPollAnswer
updateThreadName("my-chat-member")
if update.myChatMember ne null then i.onMyChatMemberUpdated
updateThreadName("chat-member")
if update.chatMember ne null then i.onChatMemberUpdated
updateThreadName("chat-join-request")
if update.chatJoinRequest ne null then i.onChatJoinRequest
} catch case e => {
val errorMessage = StringBuilder()
errorMessage ++= "Event throws unexpected exception:\n"
errorMessage ++= (exceptionLog(e) indent 4)
e match
case actionFailed: EventRuntimeException.ActionFailed =>
errorMessage ++= "\ntg-api action: response track: "
errorMessage ++= (GsonBuilder().setPrettyPrinting().create().toJson(
actionFailed.response
) indent 4) ++= "\n"
case _ =>
logger error errorMessage.toString
coeur.daemons.reporter.exception(e, "on event running")
}
} }
} }

View File

@ -2,13 +2,12 @@ package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener} import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.MornyCoeur
import com.pengrad.telegrambot.model.Update
class MornyOnUpdateTimestampOffsetLock (using coeur: MornyCoeur) extends EventListener { class MornyOnUpdateTimestampOffsetLock (using coeur: MornyCoeur) extends EventListener {
private def checkOutdated (timestamp: Int)(using event: EventEnv): Unit = private def checkOutdated (timestamp: Int)(using event: EventEnv): Unit =
if coeur.config.eventIgnoreOutdated && (timestamp < (coeur.coeurStartTimestamp/1000)) then if coeur.config.eventIgnoreOutdated && (timestamp < (coeur.coeurStartTimestamp/1000)) then
event.setEventOk event.setEventCanceled
override def onMessage (using event: EventEnv): Unit = checkOutdated(event.update.message.date) override def onMessage (using event: EventEnv): Unit = checkOutdated(event.update.message.date)
override def onEditedMessage (using event: EventEnv): Unit = checkOutdated(event.update.editedMessage.date) override def onEditedMessage (using event: EventEnv): Unit = checkOutdated(event.update.editedMessage.date)

View File

@ -5,7 +5,7 @@ import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.data.TelegramStickers
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.TelegramExtensions.Bot.exec import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import com.pengrad.telegrambot.model.{Chat, Message, Update, User} import com.pengrad.telegrambot.model.{Chat, Message, User}
import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.request.{ForwardMessage, GetChat, SendMessage, SendSticker} import com.pengrad.telegrambot.request.{ForwardMessage, GetChat, SendMessage, SendSticker}

View File

@ -2,8 +2,7 @@ package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener} import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.daemon.{MedicationTimer, MornyDaemons} import com.pengrad.telegrambot.model.Message
import com.pengrad.telegrambot.model.{Message, Update}
class OnMedicationNotifyApply (using coeur: MornyCoeur) extends EventListener { class OnMedicationNotifyApply (using coeur: MornyCoeur) extends EventListener {
@ -14,8 +13,8 @@ class OnMedicationNotifyApply (using coeur: MornyCoeur) extends EventListener {
private def editedMessageProcess (edited: Message)(using event: EventEnv): Unit = { private def editedMessageProcess (edited: Message)(using event: EventEnv): Unit = {
if edited.chat.id != coeur.config.medicationNotifyToChat then return; if edited.chat.id != coeur.config.medicationNotifyToChat then return;
coeur.daemons.medicationTimer.refreshNotificationWrite(edited) if coeur.daemons.medicationTimer.refreshNotificationWrite(edited) then
event.setEventOk event.setEventOk
} }
} }

View File

@ -4,13 +4,12 @@ import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.bot.command.MornyCommands import cc.sukazyo.cono.morny.bot.command.MornyCommands
import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyCoeur
class OnUniMeowTrigger (using commands: MornyCommands) (using coeur: MornyCoeur) extends EventListener { class OnUniMeowTrigger (using commands: MornyCommands) extends EventListener {
override def onMessage (using event: EventEnv): Unit = { override def onMessage (using event: EventEnv): Unit = {
event.consume (classOf[InputCommand]) { input => event.consume[InputCommand] { input =>
logger trace s"got input command {$input} from event-context" logger trace s"got input command {$input} from event-context"
for ((name, command_instance) <- commands.commands_uni) { for ((name, command_instance) <- commands.commands_uni) {

View File

@ -69,8 +69,8 @@ class MedicationTimer (using coeur: MornyCoeur) {
else lastNotify_messageId = None else lastNotify_messageId = None
} }
def refreshNotificationWrite (edited: Message): Unit = { def refreshNotificationWrite (edited: Message): Boolean = {
if (lastNotify_messageId isEmpty) || (lastNotify_messageId.get != (edited.messageId toInt)) then return if (lastNotify_messageId isEmpty) || (lastNotify_messageId.get != (edited.messageId toInt)) then return false
import cc.sukazyo.cono.morny.util.CommonFormat.formatDate import cc.sukazyo.cono.morny.util.CommonFormat.formatDate
val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60) val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60)
val entities = ArrayBuffer.empty[MessageEntity] val entities = ArrayBuffer.empty[MessageEntity]
@ -82,6 +82,7 @@ class MedicationTimer (using coeur: MornyCoeur) {
edited.text + s"\n-- $editTime --" edited.text + s"\n-- $editTime --"
).entities(entities toArray:_*) ).entities(entities toArray:_*)
lastNotify_messageId = None lastNotify_messageId = None
true
} }
} }

View File

@ -134,10 +134,13 @@ class MornyReport (using coeur: MornyCoeur) {
object EventStatistics { object EventStatistics {
private var eventTotal = 0 private var eventTotal = 0
private var eventCanceled = 0
private val runningTime: NumericStatistics[DurationMillis] = NumericStatistics() private val runningTime: NumericStatistics[DurationMillis] = NumericStatistics()
def reset (): Unit = { def reset (): Unit = {
eventTotal = 0; runningTime.reset() eventTotal = 0
eventCanceled = 0
runningTime.reset()
} }
private def runningTimeStatisticsHTML: String = private def runningTimeStatisticsHTML: String =
@ -154,11 +157,13 @@ class MornyReport (using coeur: MornyCoeur) {
def eventStatisticsHTML: String = def eventStatisticsHTML: String =
import cc.sukazyo.cono.morny.util.UseMath.percentageOf as p import cc.sukazyo.cono.morny.util.UseMath.percentageOf as p
val processed = runningTime.count val processed = runningTime.count
val ignored = eventTotal - processed val canceled = eventCanceled
val ignored = eventTotal - processed - canceled
// language=html // language=html
s""" - <i>total event received</i>: <code>$eventTotal</code> 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>event ignored</i>: (<code>${eventTotal p ignored}%</code>) <code>$ignored</code>
| - <i>event canceled</i>: (<code>${eventTotal p canceled}%</code>) <code>$canceled</code>
| - <i>event processed</i>: (<code>${eventTotal p processed}%</code>) <code>$processed</code>
| - <i>processed time usage</i>: | - <i>processed time usage</i>:
|${runningTimeStatisticsHTML.indent(3)}""".stripMargin |${runningTimeStatisticsHTML.indent(3)}""".stripMargin
@ -167,13 +172,23 @@ class MornyReport (using coeur: MornyCoeur) {
//noinspection ScalaWeakerAccess //noinspection ScalaWeakerAccess
case class EventTimeUsed (it: DurationMillis) case class EventTimeUsed (it: DurationMillis)
override def atEventPost (using event: EventEnv): Unit = { override def atEventPost (using event: EventEnv): Unit = {
import event.State
eventTotal += 1 eventTotal += 1
if event.isEventOk then { event.state match
val timeUsed = EventTimeUsed(System.currentTimeMillis - event.timeStartup) case State.OK(from) =>
event provide timeUsed val timeUsed = EventTimeUsed(System.currentTimeMillis - event.timeStartup)
logger debug s"event consumed ${timeUsed.it}ms" event provide timeUsed
runningTime ++ timeUsed.it logger debug
} s"""event done with OK
| with time consumed ${timeUsed.it}ms
| by $from""".stripMargin
runningTime ++ timeUsed.it
case State.CANCELED(from) =>
eventCanceled += 1
logger debug
s"""event done with CANCELED"
| by $from""".stripMargin
case null =>
} }
} }