Added code to schedule fetch tasks

This commit is contained in:
2023-10-08 12:36:48 +02:00
parent 805f0a7219
commit 154af5b7a1
11 changed files with 199 additions and 5 deletions
@@ -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
@@ -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<BahnTimetable>()
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
@@ -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"
}
}
}
@@ -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"
}
}
}
@@ -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."
}
}
}
@@ -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<HourAtDay, MutableList<ScheduledFetchTask>> = mutableMapOf()
var tasks: List<ScheduledFetchTask> = 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)
}
}
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.db
data class Platform(
var name: String,
var sections: MutableList<Section>
)
@@ -0,0 +1,5 @@
package de.twomartens.timetable.model.db
data class Section(
var name: String
)
@@ -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<Platform>
) {
@Id
var id: ObjectId = ObjectId()
@CreatedDate
lateinit var created: Instant
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -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<Eva>
) {
@Id
var id: ObjectId = ObjectId()
@CreatedDate
lateinit var created: Instant
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -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<ScheduledFetchTask>()
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<ScheduledFetchTask>) {
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 {}
}
}