Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

204 cron job for deleting old db exports and files as well as filesystem #213

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
01d68a9
wip
pavliuc75 Nov 27, 2024
48db7be
some dual read
pavliuc75 Nov 27, 2024
d8658a6
wip
pavliuc75 Dec 4, 2024
b65264a
Merge branch 'develop' into 167-restructure-file-system
pavliuc75 Dec 13, 2024
8fb18ec
wip
pavliuc75 Dec 13, 2024
32debc3
wip1
pavliuc75 Dec 16, 2024
55e565b
wip
pavliuc75 Dec 16, 2024
6948f65
formatting and etc
pavliuc75 Dec 16, 2024
cf4e4ff
Merge branch 'develop' into 167-restructure-file-system
yuanchen233 Dec 18, 2024
aa35ee3
Merge branch 'develop' into 167-restructure-file-system
yuanchen233 Dec 19, 2024
49bc1d4
wip
pavliuc75 Dec 23, 2024
2c6c64e
wiip
pavliuc75 Jan 7, 2025
38fcea1
wiip
pavliuc75 Jan 7, 2025
040cd56
wip
pavliuc75 Jan 7, 2025
5fdfd06
wip, stopped at claims
pavliuc75 Jan 8, 2025
46d5ce8
wip
pavliuc75 Jan 14, 2025
25915a8
wip
pavliuc75 Jan 14, 2025
fb06190
wip
pavliuc75 Jan 15, 2025
fae40c4
wip bebore changing db again
pavliuc75 Jan 15, 2025
be8e1f9
wip
pavliuc75 Jan 17, 2025
89a04d0
fix flyway migr
pavliuc75 Jan 17, 2025
b5130ce
Merge branch 'develop' into 167-restructure-file-system
pavliuc75 Jan 27, 2025
aeec1ed
fixed anon participans and other stuff
pavliuc75 Jan 27, 2025
18b91fd
fixed anon participans and other stuff
pavliuc75 Jan 27, 2025
78e5e62
Merge remote-tracking branch 'origin/167-restructure-file-system' int…
pavliuc75 Jan 27, 2025
2c02969
added cron jobs to delete old files and exports
pavliuc75 Jan 31, 2025
31b5ce4
Merge branch 'develop' into 204-cron-job-for-deleting-old-db-exports-…
pavliuc75 Feb 7, 2025
8d73da2
Merge branch 'develop' into 204-cron-job-for-deleting-old-db-exports-…
yuanchen233 Feb 10, 2025
aeb38d0
moved cleanup retention days arg to application yml
pavliuc75 Feb 12, 2025
0c1aa50
Merge remote-tracking branch 'origin/204-cron-job-for-deleting-old-db…
pavliuc75 Feb 12, 2025
94c8f27
Merge remote-tracking branch 'origin/204-cron-job-for-deleting-old-db…
pavliuc75 Feb 12, 2025
e333d3a
Merge remote-tracking branch 'origin/204-cron-job-for-deleting-old-db…
pavliuc75 Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ RABBIT_PORT=5672

PROMETHEUS_PORT=9090
GRAFANA_PORT=3000

FILES_RETENTION_DAYS=
EXPORTS_RETENTION_DAYS=
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ interface ExportRepository : JpaRepository<Export, String> {

@Query(nativeQuery = true, value = "SELECT * FROM exports WHERE created_at < :timestamp")
fun findAllCreatedBefore(timestamp: Instant): List<Export>

fun getAllByUpdatedAtIsBefore(updatedAtBefore: Instant): MutableList<Export>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dk.cachet.carp.webservices.export.scheduler

import dk.cachet.carp.webservices.export.service.ExportService
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
class ExportCleanup(
private val exportService: ExportService,
@Value("\${cleanup.exports.retention-days}") private val days: Int,
) {
companion object {
private val LOGGER: Logger = LogManager.getLogger()
}

@Scheduled(cron = "0 5 9 * * ?")
fun cleanup() {
LOGGER.info("Cleaning up exports...")
exportService.deleteAllOlderThan(days)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ interface ExportService {
studyId: UUID,
exportId: UUID,
): UUID

fun deleteAllOlderThan(days: Int)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.springframework.core.io.Resource
import org.springframework.stereotype.Service
import java.io.IOException
import java.nio.file.Path
import java.time.Instant

@Service
class ExportServiceImpl(
Expand Down Expand Up @@ -67,6 +69,23 @@ class ExportServiceImpl(
return studyId
}

@Suppress("MagicNumber")
override fun deleteAllOlderThan(days: Int) {
val clockNow7DaysAgo = System.currentTimeMillis() - days * 24 * 60 * 60 * 1000
val exportsToDelete = exportRepository.getAllByUpdatedAtIsBefore(Instant.ofEpochMilli(clockNow7DaysAgo))

exportsToDelete.forEach { export: Export ->
exportRepository.delete(export)
try {
fileStorage.deleteFileAtPath(export.fileName, Path.of(export.relativePath))
} catch (e: IOException) {
LOGGER.error("Failed to delete export with id ${export.id}.", e)
}
}

LOGGER.info("Exports older than $days days have been successfully deleted.")
}

/**
* Get the export with the given [id] if it exists, or delete it if it failed to export and return null.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.stereotype.Repository
import org.springframework.web.multipart.MultipartFile
import java.time.Instant

@Repository
interface FileRepository : JpaRepository<File, Int>, JpaSpecificationExecutor<File>, FileRepositoryCustom {
Expand All @@ -16,6 +17,8 @@ interface FileRepository : JpaRepository<File, Int>, JpaSpecificationExecutor<Fi
): List<File>

fun findByStudyId(studyId: String): List<File>

fun getAllByUpdatedAtIsBefore(updatedAtBefore: Instant): MutableList<File>
}

// TODO: This is not a repository, dont't be mislead by that, it needs to be moved to its own service somewhere else.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dk.cachet.carp.webservices.file.scheduler

import dk.cachet.carp.webservices.file.service.FileService
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
class FileCleanup(
private val fileService: FileService,
@Value("\${cleanup.files.retention-days}") private val days: Int,
) {
companion object {
private val LOGGER: Logger = LogManager.getLogger()
}

@Scheduled(cron = "0 0 9 * * ?")
fun cleanup() {
LOGGER.info("Cleaning up files...")
fileService.deleteAllOlderThan(days)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface FileService {
studyId: UUID,
)

fun deleteAllOlderThan(days: Int)

suspend fun deleteAllByStudyId(studyId: String)

/** The [createDEPRECATED] interface for creating a file. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ import org.springframework.transaction.annotation.Transactional
import org.springframework.util.StringUtils
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.util.UriComponentsBuilder
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant

@Service
@Transactional
// Extract S3 methods into a separate service
@Suppress("LongParameterList")
@Suppress("LongParameterList", "TooManyFunctions")
class FileServiceImpl(
private val fileRepository: FileRepository,
private val fileStorage: FileStorage,
Expand Down Expand Up @@ -166,6 +168,23 @@ class FileServiceImpl(
LOGGER.info("File deleted, id = $id")
}

@Suppress("MagicNumber")
override fun deleteAllOlderThan(days: Int) {
val clockNow7DaysAgo = System.currentTimeMillis() - days * 24 * 60 * 60 * 1000
val filesToDelete = fileRepository.getAllByUpdatedAtIsBefore(Instant.ofEpochMilli(clockNow7DaysAgo))

filesToDelete.forEach { file ->
fileRepository.delete(file)
try {
fileStorage.deleteFileAtPath(file.fileName, Path.of(file.relativePath))
} catch (e: IOException) {
LOGGER.error("Failed to delete file with id = ${file.id}", e)
}
}

LOGGER.info("All files older than $days days deleted")
}

override suspend fun deleteAllByStudyId(studyId: String) {
val files =
withContext(Dispatchers.IO) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ storage:
enabled: true
days: 90

cleanup:
files:
retention-days: "${FILES_RETENTION_DAYS:7}"
exports:
retention-days: "${EXPORTS_RETENTION_DAYS:7}"

environment:
url: "${CAWS_URL}"
portalUrl: "${CAWS_PORTAL_URL}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.assertThrows
import java.io.IOException
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertFailsWith
Expand Down Expand Up @@ -137,4 +139,77 @@ class ExportServiceTest {
verify { fileStorage.deleteFileAtPath(entry.fileName, any<Path>()) }
}
}

@Nested
inner class DeleteAllOlderThan {
@Test
fun `should delete all exports older than specified days`() {
val days = 7
val clockNow7DaysAgo = System.currentTimeMillis() - days * 24 * 60 * 60 * 1000
val exportsToDelete =
mutableListOf(
Export(
id = "1",
fileName = "file1",
relativePath = "path1",
),
Export(
id = "2",
fileName = "file2",
relativePath = "path2",
),
)

every { repository.getAllByUpdatedAtIsBefore(any()) } returns exportsToDelete
every { repository.delete(any()) } answers { nothing }
every { fileStorage.deleteFileAtPath(any(), any()) } returns true

val sut = ExportServiceImpl(repository, invoker, fileStorage)
sut.deleteAllOlderThan(days)

verify(exactly = exportsToDelete.size) { repository.delete(any()) }
verify(exactly = exportsToDelete.size) { fileStorage.deleteFileAtPath(any(), any()) }

verify {
repository.getAllByUpdatedAtIsBefore(
match {
val tolerance = 5000
Math.abs(it.toEpochMilli() - clockNow7DaysAgo) <= tolerance
},
)
}
}

@Test
fun `should continue deletion if something throws`() {
val days = 7
val exportsToDelete =
mutableListOf(
Export(
id = "1",
fileName = "file1",
relativePath = "path1",
),
Export(
id = "2",
fileName = "file2",
relativePath = "path2",
),
)

every { repository.getAllByUpdatedAtIsBefore(any()) } returns exportsToDelete
every { repository.delete(any()) } answers { nothing }
every { fileStorage.deleteFileAtPath("file1", Path.of("path1")) } returns true
every { fileStorage.deleteFileAtPath("file2", Path.of("path2")) } throws IOException(";(")

val sut = ExportServiceImpl(repository, invoker, fileStorage)
sut.deleteAllOlderThan(days)

verify(exactly = exportsToDelete.size) { repository.delete(any()) }
verify(exactly = exportsToDelete.size) { fileStorage.deleteFileAtPath(any(), any()) }
assertThrows<IOException> {
fileStorage.deleteFileAtPath("file2", Path.of("path2"))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import dk.cachet.carp.webservices.security.authorization.Claim
import dk.cachet.carp.webservices.security.authorization.service.AuthorizationService
import io.mockk.*
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.mock.web.MockMultipartFile
import org.springframework.util.StringUtils
import java.io.IOException
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
Expand Down Expand Up @@ -192,4 +195,106 @@ class FileServiceTest {
assertEquals(expectedKey, actualKey)
}
}

@Nested
inner class DeleteAllOlderThan {
@Test
fun `deletes older than 7 days`() {
val fileMock1 =
mockk<File> {
every { id } returns 1
every { fileName } returns "file1"
every { relativePath } returns "foo/bar/baz"
}
val fileMock2 =
mockk<File> {
every { id } returns 2
every { fileName } returns "file2"
every { relativePath } returns "foo/bar/qux"
}
every { fileRepository.getAllByUpdatedAtIsBefore(any()) } returns mutableListOf(fileMock1, fileMock2)
every { fileRepository.delete(any<File>()) } just runs
every { fileStorage.deleteFileAtPath(any(), any()) } returns true
val sut =
FileServiceImpl(
fileRepository,
fileStorage,
messageBase,
s3Client,
authenticationService,
s3SpaceBucket,
s3SpaceEndpoint,
)
val sevenDaysAgo = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000

sut.deleteAllOlderThan(7)

verify { fileRepository.getAllByUpdatedAtIsBefore(java.time.Instant.ofEpochMilli(sevenDaysAgo)) }
verify { fileRepository.delete(fileMock1) }
verify { fileRepository.delete(fileMock2) }
verify { fileStorage.deleteFileAtPath("file1", java.nio.file.Path.of("foo", "bar", "baz")) }
verify { fileStorage.deleteFileAtPath("file2", java.nio.file.Path.of("foo", "bar", "qux")) }
}

@Test
fun `should keep deleting even if one of the files could not be deleted from storage`() {
val fileMock1 =
mockk<File> {
every { id } returns 1
every { fileName } returns "file1"
every { relativePath } returns "foo/bar/baz"
}
val fileMock2 =
mockk<File> {
every { id } returns 2
every { fileName } returns "file2"
every { relativePath } returns "foo/bar/qux"
}
every { fileRepository.getAllByUpdatedAtIsBefore(any()) } returns mutableListOf(fileMock1, fileMock2)
every { fileRepository.delete(any<File>()) } just runs
every {
fileStorage.deleteFileAtPath(
"file1",
java.nio.file.Path.of("foo", "bar", "baz"),
)
} returns true
every {
fileStorage.deleteFileAtPath(
"file2",
java.nio.file.Path.of("foo", "bar", "qux"),
)
} throws IOException(":(")
val sut =
FileServiceImpl(
fileRepository,
fileStorage,
messageBase,
s3Client,
authenticationService,
s3SpaceBucket,
s3SpaceEndpoint,
)
val sevenDaysAgo = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000

sut.deleteAllOlderThan(7)

verify { fileRepository.getAllByUpdatedAtIsBefore(any()) }
verify { fileRepository.delete(fileMock1) }
verify { fileRepository.delete(fileMock2) }
verify { fileStorage.deleteFileAtPath("file1", java.nio.file.Path.of("foo", "bar", "baz")) }
verify { fileStorage.deleteFileAtPath("file2", java.nio.file.Path.of("foo", "bar", "qux")) }
assertThrows<IOException> {
fileStorage.deleteFileAtPath("file2", java.nio.file.Path.of("foo", "bar", "qux"))
}

verify {
fileRepository.getAllByUpdatedAtIsBefore(
match {
val tolerance = 5000
Math.abs(it.toEpochMilli() - sevenDaysAgo) <= tolerance
},
)
}
}
}
}