add scaladoc, change Long to EpochMillis, scheduler tests

- fix wrong Telegram EpochSeconds to EpochMillis conv at OnCallMe
This commit is contained in:
A.C.Sukazyo Eyre 2023-11-05 19:25:00 +08:00
parent f0d4471646
commit 9f908aa88e
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
15 changed files with 281 additions and 33 deletions

View File

@ -8,6 +8,7 @@ import cc.sukazyo.cono.morny.bot.api.EventListenerManager
import cc.sukazyo.cono.morny.bot.event.{MornyEventListeners, MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock} import cc.sukazyo.cono.morny.bot.event.{MornyEventListeners, MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock}
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 com.pengrad.telegrambot.TelegramBot import com.pengrad.telegrambot.TelegramBot
import com.pengrad.telegrambot.request.GetMe import com.pengrad.telegrambot.request.GetMe
@ -54,7 +55,7 @@ class MornyCoeur (using val config: MornyConfig) {
* *
* in milliseconds. * in milliseconds.
*/ */
val coeurStartTimestamp: Long = System.currentTimeMillis val coeurStartTimestamp: EpochMillis = System.currentTimeMillis
/** [[TelegramBot]] account of this Morny */ /** [[TelegramBot]] account of this Morny */
val account: TelegramBot = __loginResult.account val account: TelegramBot = __loginResult.account

View File

@ -73,13 +73,14 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener {
lastDinnerData.forwardFromMessageId lastDinnerData.forwardFromMessageId
) )
import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration}
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
def lastDinner_dateMillis: Long = lastDinnerData.forwardDate longValue; def lastDinner_dateMillis: EpochMillis = EpochMillis fromEpochSeconds lastDinnerData.forwardDate
coeur.account exec SendMessage( coeur.account exec SendMessage(
req.from.id, req.from.id,
"<i>on</i> <code>%s [UTC+8]</code>\n- <code>%s</code> <i>before</i>".formatted( "<i>on</i> <code>%s [UTC+8]</code>\n- <code>%s</code> <i>before</i>".formatted(
h(formatDate(lastDinner_dateMillis, 8)), h(formatDate(lastDinner_dateMillis, 8)),
h(formatDuration(lastDinner_dateMillis)) h(formatDuration(System.currentTimeMillis - lastDinner_dateMillis))
) )
).parseMode(ParseMode HTML).replyToMessageId(sendResp.message.messageId) ).parseMode(ParseMode HTML).replyToMessageId(sendResp.message.messageId)
isAllowed = true isAllowed = true

View File

@ -6,6 +6,7 @@ import cc.sukazyo.cono.morny.daemon.MedicationTimer.calcNextRoutineTimestamp
import cc.sukazyo.cono.morny.util.schedule.RoutineTask import cc.sukazyo.cono.morny.util.schedule.RoutineTask
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.CommonFormat import cc.sukazyo.cono.morny.util.CommonFormat
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
import com.pengrad.telegrambot.model.{Message, MessageEntity} import com.pengrad.telegrambot.model.{Message, MessageEntity}
import com.pengrad.telegrambot.request.{EditMessageText, SendMessage} import com.pengrad.telegrambot.request.{EditMessageText, SendMessage}
import com.pengrad.telegrambot.response.SendResponse import com.pengrad.telegrambot.response.SendResponse
@ -30,15 +31,15 @@ class MedicationTimer (using coeur: MornyCoeur) {
override def name: String = DAEMON_THREAD_NAME_DEF override def name: String = DAEMON_THREAD_NAME_DEF
def calcNextSendTime: Long = def calcNextSendTime: EpochMillis =
val next_time = calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour) val next_time = calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour)
logger info s"medication timer will send next notify at ${CommonFormat.formatDate(next_time, use_timeZone.getTotalSeconds / 60 / 60)} with $use_timeZone [$next_time]" logger info s"medication timer will send next notify at ${CommonFormat.formatDate(next_time, use_timeZone.getTotalSeconds / 60 / 60)} with $use_timeZone [$next_time]"
next_time next_time
override def firstRoutineTimeMillis: Long = override def firstRoutineTimeMillis: EpochMillis =
calcNextSendTime calcNextSendTime
override def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: Long): Long | Null = override def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: EpochMillis): EpochMillis | Null =
calcNextSendTime calcNextSendTime
override def main: Unit = { override def main: Unit = {
@ -85,7 +86,7 @@ class MedicationTimer (using coeur: MornyCoeur) {
object MedicationTimer { object MedicationTimer {
@throws[IllegalArgumentException] @throws[IllegalArgumentException]
def calcNextRoutineTimestamp (baseTimeMillis: Long, zone: ZoneOffset, notifyAt: Set[Int]): Long = { def calcNextRoutineTimestamp (baseTimeMillis: EpochMillis, zone: ZoneOffset, notifyAt: Set[Int]): EpochMillis = {
if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set") if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set")
var time = LocalDateTime.ofEpochSecond( var time = LocalDateTime.ofEpochSecond(
baseTimeMillis / 1000, ((baseTimeMillis % 1000) * 1000 * 1000) toInt, baseTimeMillis / 1000, ((baseTimeMillis % 1000) * 1000 * 1000) toInt,

View File

@ -1,15 +1,16 @@
package cc.sukazyo.cono.morny.data package cc.sukazyo.cono.morny.data
import cc.sukazyo.cono.morny.util.EpochDateTime.{EpochDays, EpochMillis}
import com.pengrad.telegrambot.model.User import com.pengrad.telegrambot.model.User
import scala.language.postfixOps import scala.language.postfixOps
object MornyJrrp { object MornyJrrp {
def jrrp_of_telegramUser (user: User, timestamp: Long): Double = def jrrp_of_telegramUser (user: User, timestamp: EpochMillis): Double =
jrrp_v_xmomi(user.id, timestamp/(1000*60*60*24)) * 100.0 jrrp_v_xmomi(user.id, EpochDays fromEpochMillis timestamp) * 100.0
private def jrrp_v_xmomi (identifier: Long, dayStamp: Long): Double = private def jrrp_v_xmomi (identifier: Long, dayStamp: EpochDays): Double =
import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5 import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
java.lang.Long.parseLong(MD5(s"$identifier@$dayStamp").toHex.substring(0, 4), 16) / (0xffff toDouble) java.lang.Long.parseLong(MD5(s"$identifier@$dayStamp").toHex.substring(0, 4), 16) / (0xffff toDouble)

View File

@ -10,9 +10,6 @@ object EpochDateTime {
* aka. Milliseconds since 00:00:00 UTC on Thursday, 1 January 1970. * aka. Milliseconds since 00:00:00 UTC on Thursday, 1 January 1970.
*/ */
type EpochMillis = Long type EpochMillis = Long
/** Time duration/interval in milliseconds. */
type DurationMillis = Long
object EpochMillis: object EpochMillis:
/** convert a localtime with timezone to epoch milliseconds /** convert a localtime with timezone to epoch milliseconds
* *
@ -31,5 +28,41 @@ object EpochDateTime {
def apply (time_zone: (String, String)): EpochMillis = def apply (time_zone: (String, String)): EpochMillis =
time_zone match time_zone match
case (time, zone) => apply(time, zone) case (time, zone) => apply(time, zone)
/** Convert from [[EpochSeconds]].
*
* Due to the missing accuracy, the converted EpochMillis will
* be always in 0ms aligned.
*/
def fromEpochSeconds (epochSeconds: EpochSeconds): EpochMillis =
epochSeconds.longValue * 1000L
/** The UNIX Epoch Time in seconds.
*
* aka. Seconds since 00:00:00 UTC on Thursday, 1 January 1970.
*
* Normally is the epochSeconds = (epochMillis / 1000)
*
* Notice that, currently, it stores using [[Int]] (also the implementation
* method of Telegram), which will only store times before 2038-01-19 03:14:07.
*/
type EpochSeconds = Int
/** The UNIX Epoch Time in day.
*
* aka. days since 00:00:00 UTC on Thursday, 1 January 1970.
*
* Normally is the epochDays = (epochMillis / 1000 / 60 / 60 / 24)
*
* Notice that, currently, it stores using [[Short]] (also the implementation
* method of Telegram), which will only store times before 2059-09-18.
*/
type EpochDays = Short
object EpochDays:
def fromEpochMillis (epochMillis: EpochMillis): EpochDays =
(epochMillis / (1000*60*60*24)).toShort
/** Time duration/interval in milliseconds. */
type DurationMillis = Long
} }

View File

@ -1,16 +1,18 @@
package cc.sukazyo.cono.morny.util.schedule package cc.sukazyo.cono.morny.util.schedule
import cc.sukazyo.cono.morny.util.EpochDateTime.{DurationMillis, EpochMillis}
trait DelayedTask ( trait DelayedTask (
val delayedMillis: Long val delayedMillis: DurationMillis
) extends Task { ) extends Task {
override val scheduledTimeMillis: Long = System.currentTimeMillis + delayedMillis override val scheduledTimeMillis: EpochMillis = System.currentTimeMillis + delayedMillis
} }
object DelayedTask { object DelayedTask {
def apply (_name: String, delayedMillis: Long, task: =>Unit): DelayedTask = def apply (_name: String, delayedMillis: DurationMillis, task: =>Unit): DelayedTask =
new DelayedTask (delayedMillis): new DelayedTask (delayedMillis):
override val name: String = _name override val name: String = _name
override def main: Unit = task override def main: Unit = task

View File

@ -1,24 +1,26 @@
package cc.sukazyo.cono.morny.util.schedule package cc.sukazyo.cono.morny.util.schedule
import cc.sukazyo.cono.morny.util.EpochDateTime.{DurationMillis, EpochMillis}
trait IntervalTask extends RoutineTask { trait IntervalTask extends RoutineTask {
def intervalMillis: Long def intervalMillis: DurationMillis
override def firstRoutineTimeMillis: Long = override def firstRoutineTimeMillis: EpochMillis =
System.currentTimeMillis() + intervalMillis System.currentTimeMillis() + intervalMillis
override def nextRoutineTimeMillis ( override def nextRoutineTimeMillis (
previousScheduledRoutineTimeMillis: Long previousScheduledRoutineTimeMillis: EpochMillis
): Long|Null = ): EpochMillis|Null =
previousScheduledRoutineTimeMillis + intervalMillis previousScheduledRoutineTimeMillis + intervalMillis
} }
object IntervalTask { object IntervalTask {
def apply (_name: String, _intervalMillis: Long, task: =>Unit): IntervalTask = def apply (_name: String, _intervalMillis: DurationMillis, task: =>Unit): IntervalTask =
new IntervalTask: new IntervalTask:
override def intervalMillis: Long = _intervalMillis override def intervalMillis: DurationMillis = _intervalMillis
override def name: String = _name override def name: String = _name
override def main: Unit = task override def main: Unit = task

View File

@ -1,11 +1,13 @@
package cc.sukazyo.cono.morny.util.schedule package cc.sukazyo.cono.morny.util.schedule
import cc.sukazyo.cono.morny.util.EpochDateTime.{DurationMillis, EpochMillis}
trait IntervalWithTimesTask extends IntervalTask { trait IntervalWithTimesTask extends IntervalTask {
def times: Int def times: Int
private var currentExecutedTimes = 1 private var currentExecutedTimes = 1
override def nextRoutineTimeMillis (previousScheduledRoutineTimeMillis: Long): Long | Null = override def nextRoutineTimeMillis (previousScheduledRoutineTimeMillis: EpochMillis): EpochMillis | Null =
if currentExecutedTimes >= times then if currentExecutedTimes >= times then
null null
else else
@ -16,11 +18,11 @@ trait IntervalWithTimesTask extends IntervalTask {
object IntervalWithTimesTask { object IntervalWithTimesTask {
def apply (_name: String, _intervalMillis: Long, _times: Int, task: =>Unit): IntervalWithTimesTask = def apply (_name: String, _intervalMillis: DurationMillis, _times: Int, task: =>Unit): IntervalWithTimesTask =
new IntervalWithTimesTask: new IntervalWithTimesTask:
override def name: String = _name override def name: String = _name
override def times: Int = _times override def times: Int = _times
override def intervalMillis: Long = _intervalMillis override def intervalMillis: DurationMillis = _intervalMillis
override def main: Unit = task override def main: Unit = task
} }

View File

@ -1,12 +1,45 @@
package cc.sukazyo.cono.morny.util.schedule package cc.sukazyo.cono.morny.util.schedule
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
/** The task that can execute multiple times with custom routine function.
*
* When creating a Routine Task, the task's [[firstRoutineTimeMillis]] function
* will be called and the result value will be the first task scheduled time.
*
* After every execution complete and enter the post effect, the [[nextRoutineTimeMillis]]
* function will be called, then its value will be stored as the new task's
* scheduled time and re-scheduled by its scheduler.
*/
trait RoutineTask extends Task { trait RoutineTask extends Task {
private[schedule] var currentScheduledTimeMillis: Long = firstRoutineTimeMillis private[schedule] var currentScheduledTimeMillis: EpochMillis = firstRoutineTimeMillis
override def scheduledTimeMillis: Long = currentScheduledTimeMillis
def firstRoutineTimeMillis: Long /** Next running time of this task.
*
* Should be auto generated from [[firstRoutineTimeMillis]] and
* [[nextRoutineTimeMillis]].
*/
override def scheduledTimeMillis: EpochMillis = currentScheduledTimeMillis
def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: Long): Long|Null /** The task scheduled time at initial.
*
* In the default environment, this function will only be called once
* when the task object is just created.
*/
def firstRoutineTimeMillis: EpochMillis
/** The function to calculate the next scheduled time after previous task
* routine complete.
*
* This function will be called every time the task is done once, in the
* task runner thread and the post effect scope.
*
* @param previousRoutineScheduledTimeMillis The previous task routine's
* scheduled time.
* @return The next task routine's scheduled time, or [[null]] means end
* of the task.
*/
def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: EpochMillis): EpochMillis|Null
} }

View File

@ -1,5 +1,7 @@
package cc.sukazyo.cono.morny.util.schedule package cc.sukazyo.cono.morny.util.schedule
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
import scala.annotation.targetName import scala.annotation.targetName
import scala.collection.mutable import scala.collection.mutable
@ -78,7 +80,7 @@ class Scheduler {
runtimeStatus = State.PREPARE_RUN runtimeStatus = State.PREPARE_RUN
val nextMove: Task|Long|"None" = taskList.synchronized { val nextMove: Task|EpochMillis|"None" = taskList.synchronized {
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
@ -107,7 +109,7 @@ class Scheduler {
currentRunning match currentRunning match
case routine: RoutineTask => case routine: RoutineTask =>
routine.nextRoutineTimeMillis(routine.currentScheduledTimeMillis) match routine.nextRoutineTimeMillis(routine.currentScheduledTimeMillis) match
case next: Long => case next: EpochMillis =>
routine.currentScheduledTimeMillis = next routine.currentScheduledTimeMillis = next
if (!currentRunning_isScheduledCancel) schedule(routine) if (!currentRunning_isScheduledCancel) schedule(routine)
case _ => case _ =>
@ -117,7 +119,7 @@ class Scheduler {
currentRunning = null currentRunning = null
this setName runnerName this setName runnerName
case needToWaitMillis: Long => case needToWaitMillis: EpochMillis =>
runtimeStatus = State.WAITING runtimeStatus = State.WAITING
try Thread.sleep(needToWaitMillis) try Thread.sleep(needToWaitMillis)
catch case _: InterruptedException => {} catch case _: InterruptedException => {}

View File

@ -55,3 +55,13 @@ trait Task extends Ordered[Task] {
s"""${super.toString}{"$name": $scheduledTimeMillis}""" s"""${super.toString}{"$name": $scheduledTimeMillis}"""
} }
object Task {
def apply (_name: String, _scheduledTime: EpochMillis, _main: =>Unit): Task =
new Task:
override def name: String = _name
override def scheduledTimeMillis: EpochMillis = _scheduledTime
override def main: Unit = _main
}

View File

@ -1,11 +1,55 @@
package cc.sukazyo.cono.morny.test.utils package cc.sukazyo.cono.morny.test.utils
import cc.sukazyo.cono.morny.test.MornyTests import cc.sukazyo.cono.morny.test.MornyTests
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis import cc.sukazyo.cono.morny.util.EpochDateTime.{EpochDays, EpochMillis, EpochSeconds}
import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.prop.TableDrivenPropertyChecks
class EpochDateTimeTest extends MornyTests with TableDrivenPropertyChecks { class EpochDateTimeTest extends MornyTests with TableDrivenPropertyChecks {
"while converting to EpochMillis :" - {
"from EpochSeconds :" - {
val examples = Table[EpochSeconds, EpochMillis](
("EpochSeconds", "EpochMillis"),
(1699176068, 1699176068000L),
(1699176000, 1699176000000L),
(1, 1000L),
)
forAll(examples) { (epochSeconds, epochMillis) =>
s"EpochSeconds($epochSeconds) should be converted to EpochMillis($epochMillis)" in {
(EpochMillis fromEpochSeconds epochSeconds) shouldEqual epochMillis
}
}
}
}
"while converting to EpochDays :" - {
"from EpochMillis :" - {
val examples = Table(
("EpochMillis", "EpochDays"),
(0L, 0),
(1000L, 0),
(80000000L, 0),
(90000000L, 1),
(1699176549059L, 19666)
)
forAll(examples) { (epochMillis, epochDays) =>
s"EpochMillis($epochMillis) should be converted to EpochDays($epochDays)" in {
(EpochDays fromEpochMillis epochMillis) shouldEqual epochDays
}
}
}
}
"while converting date-time string to time-millis : " - { "while converting date-time string to time-millis : " - {
"while using ISO-Offset-Date-Time : " - { "while using ISO-Offset-Date-Time : " - {

View File

@ -0,0 +1,23 @@
package cc.sukazyo.cono.morny.test.utils.schedule
import cc.sukazyo.cono.morny.test.MornyTests
import cc.sukazyo.cono.morny.util.schedule.{IntervalWithTimesTask, Scheduler}
import org.scalatest.tagobjects.Slow
class IntervalsTest extends MornyTests {
"IntervalWithTimesTest should work even scheduler is scheduled to stop" taggedAs Slow in {
val scheduler = Scheduler()
var times = 0
scheduler ++ IntervalWithTimesTask("intervals-10", 200, 10, {
times = times + 1
})
val startTime = System.currentTimeMillis()
scheduler.waitForStopAtAllDone()
val timeUsed = System.currentTimeMillis() - startTime
times shouldEqual 10
timeUsed should (be <= 2100L and be >= 1900L)
info(s"interval 200ms for 10 times used time ${timeUsed}ms")
}
}

View File

@ -0,0 +1,47 @@
package cc.sukazyo.cono.morny.test.utils.schedule
import cc.sukazyo.cono.morny.test.MornyTests
import cc.sukazyo.cono.morny.util.schedule.{DelayedTask, Scheduler, Task}
import org.scalatest.tagobjects.Slow
import scala.collection.mutable
class SchedulerTest extends MornyTests {
"While executing tasks using scheduler :" - {
"Task with scheduleTime smaller than current time should be executed immediately" in {
val scheduler = Scheduler()
var time = System.currentTimeMillis
scheduler ++ Task("task", 0, {
time = System.currentTimeMillis - time
})
scheduler.waitForStopAtAllDone()
time should be <= 10L
info(s"Immediately Task done with time $time")
}
"Task's running thread name should be task name" in {
val scheduler = Scheduler()
var executedThread: Option[String] = None
scheduler ++ Task("task", 0, {
executedThread = Some(Thread.currentThread.getName)
})
scheduler.waitForStopAtAllDone()
executedThread shouldEqual Some("task")
}
"Task's execution order should be ordered by task Ordering but not insert order" taggedAs Slow in {
val scheduler = Scheduler()
val result = mutable.ArrayBuffer.empty[String]
scheduler
++ DelayedTask("task-later", 400L, { result += Thread.currentThread.getName })
++ DelayedTask("task-very-late", 800L, { result += Thread.currentThread.getName })
++ DelayedTask("task-early", 100L, { result += Thread.currentThread.getName })
scheduler.waitForStopAtAllDone()
result.toArray shouldEqual Array("task-early", "task-later", "task-very-late")
}
}
}

View File

@ -0,0 +1,46 @@
package cc.sukazyo.cono.morny.test.utils.schedule
import cc.sukazyo.cono.morny.test.MornyTests
import cc.sukazyo.cono.morny.util.schedule.Task
import org.scalatest.tagobjects.Slow
class TaskBasicTest extends MornyTests {
"while comparing tasks :" - {
"tasks with different scheduleTime should be compared using scheduledTime" in {
Task("task-a", 21747013400912L, {}) should be > Task("task-b", 21747013400138L, {})
Task("task-a", 100L, {}) should be > Task("task-b", 99L, {})
Task("task-a", 100L, {}) should be < Task("task-b", 101, {})
Task("task-a", -19943L, {}) should be < Task("task-b", 0L, {})
}
"task with the same scheduledTime should not be equal" in {
Task("same-task?", 0L, {}) should not equal Task("same-task?", 0L, {})
}
"tasks which is only the same object should be equal" in {
def createNewTask = Task("same-task?", 0L, {})
val task1 = createNewTask
val task2 = createNewTask
val task1_copy = task1
task1 shouldEqual task1_copy
task1 should not equal task2
}
}
"task can be sync executed by calling its main method." taggedAs Slow in {
Thread.currentThread setName "parent-thread"
val data = StringBuilder("")
val task = Task("some-task", 0L, {
Thread.sleep(100)
data ++= Thread.currentThread.getName ++= " // " ++= "task-complete"
})
task.main
data.toString shouldEqual "parent-thread // task-complete"
}
}