Fix deployment for fly.io

This commit is contained in:
Guntis Smaukstelis
2025-02-13 16:11:50 +02:00
parent c3f85fb477
commit 37ac7eb0e3
11 changed files with 81 additions and 42 deletions
+5
View File
@@ -5,3 +5,8 @@ POSTGRES_PASSWORD=aaa
METEO_USER=aaa METEO_USER=aaa
METEO_PASSWORD=aaa METEO_PASSWORD=aaa
METEO_URL=aaa METEO_URL=aaa
HARMONIE_EDR_API_KEY=aaa
HARMONIE_EDR_URL=aaa
HARMONIE_STAC_API_KEY=aaa
HARMONIE_STAC_URL=aaa
+1 -1
View File
@@ -3,6 +3,6 @@ FROM amazoncorretto:17-alpine
COPY ./web/dist/ /web/dist/ COPY ./web/dist/ /web/dist/
COPY ./target/scala-2.13/WeatherTool-assembly-0.1.1-SNAPSHOT.jar /app.jar COPY ./target/scala-2.13/WeatherTool-assembly-0.1.1-SNAPSHOT.jar /app.jar
VOLUME /data VOLUME /app/data
CMD ["java", "-jar", "/app.jar"] CMD ["java", "-jar", "/app.jar"]
+2
View File
@@ -10,4 +10,6 @@ FROM amazoncorretto:17-alpine
COPY --from=build /app/web/dist/ /web/dist/ COPY --from=build /app/web/dist/ /web/dist/
COPY --from=build /app/target/scala-2.13/WeatherTool-assembly-0.1.1-SNAPSHOT.jar /app.jar COPY --from=build /app/target/scala-2.13/WeatherTool-assembly-0.1.1-SNAPSHOT.jar /app.jar
VOLUME /app/data
CMD ["java", "-jar", "/app.jar"] CMD ["java", "-jar", "/app.jar"]
+4 -3
View File
@@ -13,8 +13,9 @@ primary_region = "waw"
auto_start_machines = true auto_start_machines = true
[[mounts]] [[mounts]]
source = "data_volume" source = "file_volume"
destination = "/data" destination = "/app/data"
size_gb = 10
[[vm]] [[vm]]
memory = 512 memory = 512
@@ -22,5 +23,5 @@ primary_region = "waw"
cpus = 1 cpus = 1
[scale] [scale]
count = 0 count = 1
idle_timeout = 0 idle_timeout = 0
+5 -3
View File
@@ -19,12 +19,14 @@ object Main extends IOApp {
fetchCsvTask = fileFetchScheduler.run.compile.drain fetchCsvTask = fileFetchScheduler.run.compile.drain
scheduler <- fetchDMI.Scheduler.of scheduler <- fetchDMI.Scheduler.of
cleanupTask = scheduler.scheduleTask("Cleanup", List(1), DataService.deleteOldForecasts()).compile.drain dataService <- DataService.of
fetchGrib <- fetchDMI.FetchService.of cleanupTask = scheduler.scheduleTask("Cleanup", List(1), dataService.deleteOldForecasts()).compile.drain
fetchGrib <- fetchDMI.FetchService.of(dataService)
fetchGribTask = scheduler.scheduleTask("Fetch Grib", List(2), fetchGrib.fetchRecentForecasts()).compile.drain fetchGribTask = scheduler.scheduleTask("Fetch Grib", List(2), fetchGrib.fetchRecentForecasts()).compile.drain
server <- Server.of(postgresService, fetch) server <- Server.of(postgresService, dataService, fetch)
serverTask = server.run serverTask = server.run
exitCode <- (serverTask, fetchCsvTask, cleanupTask, fetchGribTask).parMapN((_, _, _, _) => ExitCode.Success) exitCode <- (serverTask, fetchCsvTask, cleanupTask, fetchGribTask).parMapN((_, _, _, _) => ExitCode.Success)
+31 -8
View File
@@ -4,26 +4,49 @@ import cats.effect.IO
import cats.implicits.toTraverseOps import cats.implicits.toTraverseOps
import fs2.io.file.{Files, Path} import fs2.io.file.{Files, Path}
import grib.{Grib, GribParser} import grib.{Grib, GribParser}
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger
import java.nio.file.Paths
import java.time.{ZoneId, ZoneOffset, ZonedDateTime} import java.time.{ZoneId, ZoneOffset, ZonedDateTime}
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import scala.io.Source import scala.io.Source
import scala.util.Try import scala.util.Try
object DataService { object DataService {
val FOLDER = "data" 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"
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))
_ <- log.info(s"Created directories: $BASE_FOLDER and $GRIB_FOLDER")
} yield ()
}
def getFileList(): IO[List[String]] = def getFileList(): IO[List[String]] =
Files[IO] Files[IO]
.list(Path(FOLDER)) .list(Path(GRIB_FOLDER))
.map(_.toString) .map(_.toString)
.filter(_.endsWith(".grib")) .filter(_.endsWith(".grib"))
.map(_.replace(s"$FOLDER/", "")) .map(_.replace(s"$GRIB_FOLDER/", ""))
.compile .compile
.toList .toList
def getGribStucture(fileName: String): IO[List[Grib]] = { def getGribStucture(fileName: String): IO[List[Grib]] = {
val filePath = Path(s"$FOLDER/$fileName") val filePath = Path(s"$GRIB_FOLDER/$fileName")
Files[IO].exists(filePath).flatMap { Files[IO].exists(filePath).flatMap {
case true => GribParser.parseFile(filePath) case true => GribParser.parseFile(filePath)
@@ -33,7 +56,7 @@ object DataService {
def getBinaryChunk(offset: Int, length: Int, fileName: String): IO[Array[Byte]] = { def getBinaryChunk(offset: Int, length: Int, fileName: String): IO[Array[Byte]] = {
IO { IO {
val source = Source.fromFile(s"$FOLDER/$fileName", "ISO-8859-1") val source = Source.fromFile(s"$GRIB_FOLDER/$fileName", "ISO-8859-1")
try { try {
source.slice(offset, offset + length).map(_.toByte).toArray source.slice(offset, offset + length).map(_.toByte).toArray
} finally { } finally {
@@ -54,14 +77,14 @@ object DataService {
val oldThreshold = nowUTC.minusHours(maxHours) val oldThreshold = nowUTC.minusHours(maxHours)
for { for {
_ <- IO.println("start cleanup") _ <- log.info("start cleanup")
fileList <- getFileList() fileList <- getFileList()
fileDateList = fileList.flatMap(fileName => fileDateList = fileList.flatMap(fileName =>
getTimeFromName(fileName).map(extracted => (fileName, extracted._1)) getTimeFromName(fileName).map(extracted => (fileName, extracted._1))
) )
deleteList = fileDateList.filter(_._2.isBefore(oldThreshold)).map(_._1) deleteList = fileDateList.filter(_._2.isBefore(oldThreshold)).map(_._1)
_ <- deleteList.traverse(name => Files[IO].delete(Path(s"$FOLDER/${name}"))) _ <- deleteList.traverse(name => Files[IO].delete(Path(s"$GRIB_FOLDER/${name}")))
_ <- deleteList.traverse(name => IO.println(s"delete: $name")) _ <- deleteList.traverse(name => log.info(s"delete: $name"))
} yield deleteList } yield deleteList
} }
+5 -3
View File
@@ -12,8 +12,9 @@ object DataServiceTest {
private def deleteOldForecasts(): IO[Unit] = { private def deleteOldForecasts(): IO[Unit] = {
val program = for { val program = for {
deleteList <- DataService.deleteOldForecasts() dataService <- DataService.of
// _ <- IO.println(deleteList.asJson) deleteList <- dataService.deleteOldForecasts()
_ <- IO.println(deleteList.asJson)
} yield () } yield ()
program program
@@ -21,7 +22,8 @@ object DataServiceTest {
private def getForecasts(): IO[Unit] = { private def getForecasts(): IO[Unit] = {
val program = for { val program = for {
forecasts <- DataService.getForecasts() dataService <- DataService.of
forecasts <- dataService.getForecasts()
_ <- IO.println(forecasts.asJson) _ <- IO.println(forecasts.asJson)
} yield () } yield ()
+9 -8
View File
@@ -22,12 +22,12 @@ final case class HarmonieServerConfig(
) )
object FetchService { object FetchService {
def of: IO[FetchService] = { def of(dataService: DataService): IO[FetchService] = {
Slf4jLogger.create[IO].map(logger => new FetchService(logger)) Slf4jLogger.create[IO].map(logger => new FetchService(dataService, logger))
} }
} }
class FetchService(log: Logger[IO]) { class FetchService(dataService: DataService, log: Logger[IO]) {
private val edrConfig: HarmonieServerConfig = ( private val edrConfig: HarmonieServerConfig = (
sys.env.get("HARMONIE_EDR_API_KEY"), sys.env.get("HARMONIE_EDR_API_KEY"),
sys.env.get("HARMONIE_EDR_URL"), sys.env.get("HARMONIE_EDR_URL"),
@@ -35,7 +35,7 @@ class FetchService(log: Logger[IO]) {
case (Some(api_key), Some(url)) => case (Some(api_key), Some(url)) =>
HarmonieServerConfig(api_key, url) HarmonieServerConfig(api_key, url)
case _ => case _ =>
throw new RuntimeException("Unable to load harmonie config: Missing required environment variables") throw new RuntimeException("Unable to load harmonie edr config: Missing required environment variables")
} }
private val stacConfig: HarmonieServerConfig = ( private val stacConfig: HarmonieServerConfig = (
@@ -45,7 +45,7 @@ class FetchService(log: Logger[IO]) {
case (Some(api_key), Some(url)) => case (Some(api_key), Some(url)) =>
HarmonieServerConfig(api_key, url) HarmonieServerConfig(api_key, url)
case _ => case _ =>
throw new RuntimeException("Unable to load harmonie config: Missing required environment variables") throw new RuntimeException("Unable to load harmonie stac config: Missing required environment variables")
} }
/* /*
@@ -77,7 +77,8 @@ class FetchService(log: Logger[IO]) {
urlWithParams = Uri.unsafeFromString(s"${base.toString}?${queryParams.toString}") urlWithParams = Uri.unsafeFromString(s"${base.toString}?${queryParams.toString}")
request = Request[IO](Method.GET, urlWithParams) request = Request[IO](Method.GET, urlWithParams)
tmpPath = Path(s"${DataService.FOLDER}/tmp.grib") // TODO proly better to call dataService method than property
tmpPath = Path(s"${dataService.GRIB_FOLDER}/tmp.grib")
_ <- client.stream(request) _ <- client.stream(request)
.flatMap(_.body) .flatMap(_.body)
.through(Files[IO].writeAll(tmpPath)) .through(Files[IO].writeAll(tmpPath))
@@ -85,7 +86,7 @@ class FetchService(log: Logger[IO]) {
.drain .drain
gribList <- GribParser.parseFile(tmpPath) gribList <- GribParser.parseFile(tmpPath)
gribTime = gribList.head.time gribTime = gribList.head.time
fileName = Path(s"${DataService.FOLDER}/harmonie_${gribTime.referenceTime}_${gribTime.forecastTime}.grib".replace(":", "")) fileName = Path(s"${dataService.GRIB_FOLDER}/harmonie_${gribTime.referenceTime}_${gribTime.forecastTime}.grib".replace(":", ""))
_ <- Files[IO].move(tmpPath, fileName, CopyFlags.apply(CopyFlag.ReplaceExisting)) _ <- Files[IO].move(tmpPath, fileName, CopyFlags.apply(CopyFlag.ReplaceExisting))
fileSizeBytes <- Files[IO].size(fileName) fileSizeBytes <- Files[IO].size(fileName)
fileSizeMB = fileSizeBytes.toDouble / (1024 * 1024) fileSizeMB = fileSizeBytes.toDouble / (1024 * 1024)
@@ -111,7 +112,7 @@ class FetchService(log: Logger[IO]) {
for { for {
availableResult <- fetchAvailableForecasts() availableResult <- fetchAvailableForecasts()
(modelRun, forecastDateList) = availableResult (modelRun, forecastDateList) = availableResult
localForecasts <- DataService.getForecasts() localForecasts <- dataService.getForecasts()
toFetchList = forecastDateList.filter(dateTime => !localForecasts.contains((modelRun, dateTime))) toFetchList = forecastDateList.filter(dateTime => !localForecasts.contains((modelRun, dateTime)))
} yield toFetchList } yield toFetchList
} }
@@ -2,6 +2,7 @@ package fetchDMI;
import cats.effect.IO import cats.effect.IO
import cats.effect.unsafe.implicits.global import cats.effect.unsafe.implicits.global
import data.DataService
import java.time.{ZoneOffset, ZonedDateTime} import java.time.{ZoneOffset, ZonedDateTime}
@@ -15,7 +16,8 @@ object FetchServiceTest {
private def fetchRecentForecasts(): IO[Unit] = { private def fetchRecentForecasts(): IO[Unit] = {
val program = for { val program = for {
fetch <- FetchService.of dataService <- DataService.of
fetch <- FetchService.of(dataService)
result <- fetch.fetchRecentForecasts() result <- fetch.fetchRecentForecasts()
_ <- IO.println("-=fetch finished=-") _ <- IO.println("-=fetch finished=-")
} yield () } yield ()
@@ -24,7 +26,8 @@ object FetchServiceTest {
private def generateFetchList(): IO[Unit] = { private def generateFetchList(): IO[Unit] = {
val program = for { val program = for {
fetch <- FetchService.of dataService <- DataService.of
fetch <- FetchService.of(dataService)
list <- fetch.generateFetchList() list <- fetch.generateFetchList()
_ <- IO.println(list) _ <- IO.println(list)
} yield () } yield ()
@@ -33,7 +36,8 @@ object FetchServiceTest {
private def fetchAvailableForecasts(): IO[Unit] = { private def fetchAvailableForecasts(): IO[Unit] = {
val program = for { val program = for {
fetch <- FetchService.of dataService <- DataService.of
fetch <- FetchService.of(dataService)
result <- fetch.fetchAvailableForecasts() result <- fetch.fetchAvailableForecasts()
(modelRun, forecastTimes) = result (modelRun, forecastTimes) = result
_ <- IO.println(modelRun) _ <- IO.println(modelRun)
@@ -48,7 +52,8 @@ object FetchServiceTest {
nowUTC <- IO(ZonedDateTime.now(ZoneOffset.UTC)) nowUTC <- IO(ZonedDateTime.now(ZoneOffset.UTC))
referenceTime = FileName.getClosestReferenceTime(nowUTC) referenceTime = FileName.getClosestReferenceTime(nowUTC)
timeList = FileName.generateTimeList(referenceTime) timeList = FileName.generateTimeList(referenceTime)
fetch <- FetchService.of dataService <- DataService.of
fetch <- FetchService.of(dataService)
nameList <- fetch.fetchFromList(timeList) nameList <- fetch.fetchFromList(timeList)
_ <- IO.println(nameList) _ <- IO.println(nameList)
} yield () } yield ()
+3 -5
View File
@@ -12,12 +12,10 @@ object GribParserTest {
val gribTitle = Codes.codesToString(0, 0, 2) val gribTitle = Codes.codesToString(0, 0, 2)
println(gribTitle) println(gribTitle)
// val fileName = s"${DataService.FOLDER}/HARMONIE_DINI_SF_2025-01-24T030000Z_2025-01-26T010000Z.grib"
val fileName = s"${DataService.FOLDER}/harmonie_2025-02-01T1500Z_2025-02-01T180000Z.grib"
val path = Path(fileName)
val program = for { val program = for {
dataService <- DataService.of
fileName = s"${dataService.GRIB_FOLDER}/harmonie_2025-02-01T1500Z_2025-02-01T180000Z.grib"
path = Path(fileName)
gribList <- GribParser.parseFile(path) gribList <- GribParser.parseFile(path)
json = gribList.asJson json = gribList.asJson
_ <- IO.println(json.spaces2) _ <- IO.println(json.spaces2)
+6 -6
View File
@@ -31,14 +31,14 @@ import scala.concurrent.duration.DurationInt
object Server { object Server {
def of(postgresService: PostgresService, fetch: FetchService): IO[Server] = { def of(postgresService: PostgresService, dataService: DataService, fetch: FetchService): IO[Server] = {
Slf4jLogger.create[IO].map { Slf4jLogger.create[IO].map {
new Server(postgresService, fetch, _) new Server(postgresService, dataService, fetch, _)
} }
} }
} }
class Server(postgresService: PostgresService, fetch: FetchService, log: Logger[IO]) { class Server(postgresService: PostgresService, dataService: DataService, fetch: FetchService, log: Logger[IO]) {
// Define the extension method `pretty` for Json // Define the extension method `pretty` for Json
implicit class JsonPrettyPrinter(json: Json) { implicit class JsonPrettyPrinter(json: Json) {
@@ -54,17 +54,17 @@ class Server(postgresService: PostgresService, fetch: FetchService, log: Logger[
// http://0.0.0.0:8080/api/show/grib-name/harmonie_2025-02-01T1500Z_2025-02-01T180000Z.grib // http://0.0.0.0:8080/api/show/grib-name/harmonie_2025-02-01T1500Z_2025-02-01T180000Z.grib
case GET -> Root / "show" / "grib" / fileName => case GET -> Root / "show" / "grib" / fileName =>
// val fileName = "data/HARMONIE_DINI_SF_2025-01-24T030000Z_2025-01-26T010000Z.grib" // val fileName = "data/HARMONIE_DINI_SF_2025-01-24T030000Z_2025-01-26T010000Z.grib"
DataService.getGribStucture(fileName).flatMap(response => Ok(response.asJson.pretty)) dataService.getGribStucture(fileName).flatMap(response => Ok(response.asJson.pretty))
case GET -> Root / "show" / "gribName" => Ok("{\"fileName\":\"TODO replace this fake name\"}") case GET -> Root / "show" / "gribName" => Ok("{\"fileName\":\"TODO replace this fake name\"}")
// http://0.0.0.0:8080/api/show/grib-list // http://0.0.0.0:8080/api/show/grib-list
case GET -> Root / "show" / "grib-list" => case GET -> Root / "show" / "grib-list" =>
DataService.getFileList().flatMap(fileList => Ok(fileList.asJson)) dataService.getFileList().flatMap(fileList => Ok(fileList.asJson))
// TODO implement binary-chunk/ get request // TODO implement binary-chunk/ get request
case GET -> Root / "grib" / "binary-chunk" / ValidateInt(binaryOffset) / ValidateInt(binaryLength) / fileName => case GET -> Root / "grib" / "binary-chunk" / ValidateInt(binaryOffset) / ValidateInt(binaryLength) / fileName =>
DataService.getBinaryChunk(binaryOffset, binaryLength, fileName).flatMap(buffer => Ok(buffer)) dataService.getBinaryChunk(binaryOffset, binaryLength, fileName).flatMap(buffer => Ok(buffer))