From 154af5b7a12eecdda02f3c9fdbc02f9bb24cae4d Mon Sep 17 00:00:00 2001 From: Jim Martens Date: Sun, 8 Oct 2023 12:36:48 +0200 Subject: [PATCH] Added code to schedule fetch tasks --- .../bahnApi/model/db/ScheduledFetchTask.kt | 7 +-- .../bahnApi/service/BahnApiService.kt | 7 ++- .../de/twomartens/timetable/model/Eva.kt | 10 +++++ .../de/twomartens/timetable/model/Hour.kt | 10 +++++ .../twomartens/timetable/model/HourAtDay.kt | 12 ++++++ .../de/twomartens/timetable/model/Tasks.kt | 42 ++++++++++++++++++ .../twomartens/timetable/model/db/Platform.kt | 6 +++ .../twomartens/timetable/model/db/Section.kt | 5 +++ .../twomartens/timetable/model/db/Station.kt | 26 +++++++++++ .../twomartens/timetable/model/db/TswRoute.kt | 36 ++++++++++++++++ .../timetable/service/ScheduledTaskService.kt | 43 +++++++++++++++++++ 11 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/model/Eva.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/model/Hour.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/model/HourAtDay.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/model/Tasks.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/model/db/Platform.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/model/db/Section.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/model/db/Station.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/model/db/TswRoute.kt diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/ScheduledFetchTask.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/ScheduledFetchTask.kt index 3a84546..ac7cd20 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/ScheduledFetchTask.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/ScheduledFetchTask.kt @@ -1,5 +1,7 @@ package de.twomartens.timetable.bahnApi.model.db +import de.twomartens.timetable.model.Eva +import de.twomartens.timetable.model.Hour import org.bson.types.ObjectId import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.Id @@ -9,14 +11,13 @@ import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime -import java.time.LocalTime @Document @CompoundIndex(def = "{'eva': 1, 'fetchedDate': 1, 'fetchedTime': 1", unique = true) data class ScheduledFetchTask( - var eva: Int, + var eva: Eva, var fetchedDate: LocalDate, - var fetchedTime: LocalTime, + var fetchedTime: Hour, var scheduledExecutionDateTime: LocalDateTime ) { @Id diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt index 0ec7e18..3459b15 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt @@ -4,6 +4,8 @@ import de.twomartens.timetable.bahnApi.model.dto.BahnStation import de.twomartens.timetable.bahnApi.model.dto.BahnStations import de.twomartens.timetable.bahnApi.model.dto.BahnTimetable import de.twomartens.timetable.bahnApi.property.BahnApiProperties +import de.twomartens.timetable.model.Eva +import de.twomartens.timetable.model.Hour import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod @@ -31,13 +33,14 @@ class BahnApiService( return body?.stations ?: listOf() } - fun fetchTimetable(eva: Int, date: LocalDate, hour: LocalTime): BahnTimetable { + fun fetchTimetable(eva: Eva, date: LocalDate, hour: Hour): BahnTimetable { val requestEntity = buildRequestEntity() val dateFormatter = DateTimeFormatter.ofPattern("yyMMdd") val timeFormatter = DateTimeFormatter.ofPattern("HH") + val time = LocalTime.of(hour.value, 0) val response = restTemplate.exchange( "https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1/plan/" + - "${eva}/${date.format(dateFormatter)}/${hour.format(timeFormatter)}", + "${eva}/${date.format(dateFormatter)}/${time.format(timeFormatter)}", HttpMethod.GET, requestEntity, BahnTimetable::class.java diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/Eva.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/Eva.kt new file mode 100644 index 0000000..7b946e5 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/Eva.kt @@ -0,0 +1,10 @@ +package de.twomartens.timetable.model + +@JvmInline +value class Eva(val value: Int) { + init { + require(value in 8000000..8999999) { + "Eva number must start with an 8 and have 7 digits" + } + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/Hour.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/Hour.kt new file mode 100644 index 0000000..7a788d6 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/Hour.kt @@ -0,0 +1,10 @@ +package de.twomartens.timetable.model + +@JvmInline +value class Hour(val value: Int) { + init { + require(value in 0..23) { + "Hour must be between 0 and 23" + } + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/HourAtDay.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/HourAtDay.kt new file mode 100644 index 0000000..5bce382 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/HourAtDay.kt @@ -0,0 +1,12 @@ +package de.twomartens.timetable.model + +import java.time.LocalDateTime + +@JvmInline +value class HourAtDay(val value: LocalDateTime) { + init { + require(value.minute == 0 && value.second == 0 && value.nano == 0) { + "DateTime must only include an hour and the date." + } + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/Tasks.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/Tasks.kt new file mode 100644 index 0000000..83d323e --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/Tasks.kt @@ -0,0 +1,42 @@ +package de.twomartens.timetable.model + +import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +private const val SECONDS_BETWEEN_TASKS = 2 + +class Tasks { + private val _tasks: MutableMap> = mutableMapOf() + var tasks: List = listOf() + get() { + return _tasks.flatMap { + it.value + } + } + set(newTasks) { + _tasks.clear() + newTasks.forEach { addTask(it) } + field = newTasks + } + + fun createAndAddTask(eva: Eva, date: LocalDate, hour: Hour): ScheduledFetchTask { + val hourAtDay = HourAtDay(date.atTime(hour.value, 0)) + val tasksForHourAtDay = _tasks.computeIfAbsent(hourAtDay) { mutableListOf() } + val secondInHourToExecute = tasksForHourAtDay.size * SECONDS_BETWEEN_TASKS + val fetchDateTime = hourAtDay.value.plusSeconds(secondInHourToExecute.toLong()) + val task = ScheduledFetchTask(eva, date, hour, fetchDateTime) + + tasksForHourAtDay.add(task) + + return task + } + + private fun addTask(scheduledFetchTask: ScheduledFetchTask) { + val scheduledTime = scheduledFetchTask.scheduledExecutionDateTime + val hourAtDay = HourAtDay(scheduledTime.truncatedTo(ChronoUnit.HOURS)) + val tasksForHourAtDay = _tasks.computeIfAbsent(hourAtDay) { mutableListOf() } + + tasksForHourAtDay.add(scheduledFetchTask) + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Platform.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Platform.kt new file mode 100644 index 0000000..dc81689 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Platform.kt @@ -0,0 +1,6 @@ +package de.twomartens.timetable.model.db + +data class Platform( + var name: String, + var sections: MutableList
+) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Section.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Section.kt new file mode 100644 index 0000000..6cb33b9 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Section.kt @@ -0,0 +1,5 @@ +package de.twomartens.timetable.model.db + +data class Section( + var name: String +) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Station.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Station.kt new file mode 100644 index 0000000..b08bdb2 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Station.kt @@ -0,0 +1,26 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.Eva +import org.bson.types.ObjectId +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.mapping.Document +import java.time.Instant + +@Document +data class Station( + @Indexed(unique = true) var eva: Eva, + var name: String, + var platforms: List +) { + @Id + var id: ObjectId = ObjectId() + + @CreatedDate + lateinit var created: Instant + + @LastModifiedDate + lateinit var lastModified: Instant +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TswRoute.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TswRoute.kt new file mode 100644 index 0000000..b70609c --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TswRoute.kt @@ -0,0 +1,36 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.Eva +import org.bson.types.ObjectId +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.mapping.Document +import java.time.Instant + +@Document +data class TswRoute( + @Indexed(unique = true) var name: String, + /** + * Ordered list of stations that represent the order of stations on the route. + * + * Not every stop is necessarily called at by each train. There may be multiple + * parallel paths through a route (like S-Bahn track and long-distance track). + * In such cases, the S-Bahn stops are listed as part of the route as they appear + * geographically. + * + * Only the station ids are used here as the duplication would be heavy and + * consistency hard to ensure when potentially repeated across various routes. + */ + var stations: List +) { + @Id + var id: ObjectId = ObjectId() + + @CreatedDate + lateinit var created: Instant + + @LastModifiedDate + lateinit var lastModified: Instant +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/service/ScheduledTaskService.kt b/module-server/src/main/kotlin/de/twomartens/timetable/service/ScheduledTaskService.kt index c7412c3..bb0d431 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/service/ScheduledTaskService.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/service/ScheduledTaskService.kt @@ -1,14 +1,20 @@ package de.twomartens.timetable.service +import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask import de.twomartens.timetable.bahnApi.repository.ScheduledFetchTaskRepository import de.twomartens.timetable.bahnApi.service.BahnApiService import de.twomartens.timetable.bahnApi.service.BahnDatabaseService import de.twomartens.timetable.bahnApi.tasks.DeleteScheduledTask import de.twomartens.timetable.bahnApi.tasks.FetchTimetableTask +import de.twomartens.timetable.model.Hour +import de.twomartens.timetable.model.Tasks +import de.twomartens.timetable.model.db.TswRoute import de.twomartens.timetable.property.TimeProperties +import mu.KotlinLogging import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler import org.springframework.stereotype.Service +import java.time.LocalDate @Service class ScheduledTaskService( @@ -19,8 +25,41 @@ class ScheduledTaskService( private val bahnDatabaseService: BahnDatabaseService, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor ) { + private val tasks: Tasks = Tasks() + fun initializeScheduledTasks() { + log.info { "Initialize persisted scheduled fetch tasks" } val scheduledTasks = scheduledFetchTaskRepository.findAll() + tasks.tasks = scheduledTasks + scheduleTasks(scheduledTasks) + } + + fun scheduleTimetableFetch(tswRoute: TswRoute, fetchedDate: LocalDate) { + log.info { + "Schedule timetable fetch: [route ${tswRoute.name}]" + } + val previousDay = fetchedDate.minusDays(1) + val nextDay = fetchedDate.plusDays(1) + val newTasks = mutableListOf() + tswRoute.stations.forEach { + val eva = it + var newTask = tasks.createAndAddTask(eva, date = previousDay, hour = Hour(23)) + newTasks.add(newTask) + for (hour in 0..23) { + newTask = tasks.createAndAddTask(eva, date = fetchedDate, hour = Hour(hour)) + newTasks.add(newTask) + } + for (hour in 0..3) { + newTask = tasks.createAndAddTask(eva, date = nextDay, hour = Hour(hour)) + newTasks.add(newTask) + } + } + + scheduledFetchTaskRepository.saveAll(newTasks) + scheduleTasks(newTasks) + } + + private fun scheduleTasks(scheduledTasks: List) { scheduledTasks.forEach { val zonedExecutionTime = it.scheduledExecutionDateTime .atZone(timeProperties.defaultTimeZone) @@ -31,4 +70,8 @@ class ScheduledTaskService( threadPoolTaskScheduler.schedule(deleteTask, zonedExecutionTime.toInstant()) } } + + companion object { + private val log = KotlinLogging.logger {} + } } \ No newline at end of file