Files
WeatherTool/src/main/scala/data/DataService.scala
T

140 lines
4.4 KiB
Scala
Raw Normal View History

package data
import cats.effect.IO
import cats.effect.implicits.concurrentParTraverseOps
import cats.effect.kernel.Resource
import cats.effect.std.Semaphore
import cats.effect.unsafe.implicits.global
import cats.implicits._
import fs2.io.file.{Files, Path}
2025-02-13 16:11:50 +02:00
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger
import parse.grib.{Grib, GribParser}
import java.io.RandomAccessFile
import java.time.{ZoneId, ZoneOffset, ZonedDateTime}
import java.time.format.DateTimeFormatter
import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
import scala.util.Try
object DataService {
2025-02-13 16:11:50 +02:00
def of: IO[DataService] = {
for {
logger <- Slf4jLogger.create[IO]
service = new DataService(logger)
_ <- service.init
} yield service
}
}
class DataService(log: Logger[IO]) {
val BASE_FOLDER = "data"
val GRIB_FOLDER = s"$BASE_FOLDER/grib"
2025-02-23 20:21:11 +02:00
val TMP_FOLDER = s"$BASE_FOLDER/tmp"
2025-02-13 16:11:50 +02:00
def init: IO[Unit] = {
for {
_ <- fs2.io.file.Files[IO].createDirectories(fs2.io.file.Path(BASE_FOLDER))
_ <- fs2.io.file.Files[IO].createDirectories(fs2.io.file.Path(GRIB_FOLDER))
2025-02-23 20:21:11 +02:00
_ <- fs2.io.file.Files[IO].createDirectories(fs2.io.file.Path(TMP_FOLDER))
_ <- log.info(s"Created directories: $BASE_FOLDER, $GRIB_FOLDER, ${TMP_FOLDER}")
2025-02-13 16:11:50 +02:00
} yield ()
}
def getFileList(): IO[List[String]] =
Files[IO]
2025-02-13 16:11:50 +02:00
.list(Path(GRIB_FOLDER))
.map(_.toString)
.filter(_.endsWith(".grib"))
2025-02-13 16:11:50 +02:00
.map(_.replace(s"$GRIB_FOLDER/", ""))
.compile
.toList
def getGribStucture(fileName: String): IO[List[Grib]] = {
2025-02-13 16:11:50 +02:00
val filePath = Path(s"$GRIB_FOLDER/$fileName")
Files[IO].exists(filePath).flatMap {
case true => GribParser.parseFile(filePath)
case false => IO.raiseError(new Exception(s"File not found: $fileName"))
}
}
def getAllFileStructure(): IO[List[Grib]] = {
for {
fileList <- getFileList()
allStructure <- fileList.parTraverseN(4)(getGribStucture) // limit concurrency
} yield allStructure.flatten
}
private val semaphore = Semaphore[IO](4).unsafeRunSync()
private val blockingEC = ExecutionContext.fromExecutorService(
Executors.newFixedThreadPool(4) // limit concurrency
)
def getBinaryChunk(offset: Int, length: Int, fileName: String): IO[Array[Byte]] = {
val fileResource = Resource.make(
IO.blocking(new RandomAccessFile(s"$GRIB_FOLDER/$fileName", "r"))
)(file => IO.blocking(file.close()))
val logMemory = IO {
val runtime = Runtime.getRuntime
val usedMemoryMB = (runtime.totalMemory - runtime.freeMemory) / 1024 / 1024
val maxMemoryMB = runtime.maxMemory / 1024 / 1024
println(s"Memory usage before reading $fileName: $usedMemoryMB MB / $maxMemoryMB MB max")
}
for {
2025-02-26 13:32:07 +02:00
// _ <- logMemory
result <- fileResource.use { file =>
IO.blocking {
val buffer = new Array[Byte](length)
file.seek(offset)
file.readFully(buffer)
buffer
}.evalOn(blockingEC)
}
_ <- IO { System.gc() }
} yield result
}
def getForecasts(): IO[List[(ZonedDateTime, ZonedDateTime)]] = {
getFileList()
.map { fileNameList =>
fileNameList.flatMap(getTimeFromName)
}
}
2025-02-11 11:33:27 +02:00
def deleteOldForecasts(maxHours: Int = 9): IO[List[String]] = {
val nowUTC = ZonedDateTime.now(ZoneOffset.UTC)
2025-02-14 14:59:07 +02:00
val ageThreshold = nowUTC.minusHours(maxHours)
2025-02-11 11:33:27 +02:00
for {
2025-02-13 16:11:50 +02:00
_ <- log.info("start cleanup")
2025-02-11 11:33:27 +02:00
fileList <- getFileList()
fileDateList = fileList.flatMap(fileName =>
getTimeFromName(fileName).map(extracted => (fileName, extracted._1))
)
2025-02-14 14:59:07 +02:00
keepList = fileDateList.filter(_._2.isAfter(ageThreshold)).map(_._1)
deleteList = fileList.filter(!keepList.contains(_))
2025-02-13 16:11:50 +02:00
_ <- deleteList.traverse(name => Files[IO].delete(Path(s"$GRIB_FOLDER/${name}")))
_ <- deleteList.traverse(name => log.info(s"delete: $name"))
2025-02-11 11:33:27 +02:00
} yield deleteList
}
private def getTimeFromName(filename: String): Option[(ZonedDateTime, ZonedDateTime)] = {
Try {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HHmm'Z'").withZone(ZoneId.of("UTC"))
val start = filename.slice(9, 25) // gets "2025-02-05T1500Z"
val end = filename.slice(26, 42) // gets "2025-02-05T1800Z"
(
ZonedDateTime.parse(start, formatter).toInstant.atZone(ZoneOffset.UTC),
ZonedDateTime.parse(end, formatter).toInstant.atZone(ZoneOffset.UTC)
)
}.toOption
}
}