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

Add service to run maintenance jobs #1034

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ dependencies {
implementation(projects.services.authorizationService)
implementation(projects.services.hierarchyService)
implementation(projects.services.infrastructureService)
implementation(projects.services.maintenanceService)
implementation(projects.services.reportStorageService)
implementation(projects.services.secretService)
implementation(projects.storage.storageSpi)
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/kotlin/di/Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import org.eclipse.apoapsis.ortserver.services.ReportStorageService
import org.eclipse.apoapsis.ortserver.services.RepositoryService
import org.eclipse.apoapsis.ortserver.services.SecretService
import org.eclipse.apoapsis.ortserver.services.VulnerabilityService
import org.eclipse.apoapsis.ortserver.services.maintenance.MaintenanceService
import org.eclipse.apoapsis.ortserver.storage.Storage

import org.koin.core.module.dsl.singleOf
Expand Down Expand Up @@ -117,6 +118,8 @@ fun ortServerModule(config: ApplicationConfig) = module {
val keycloakGroupPrefix = get<ApplicationConfig>().tryGetString("keycloak.groupPrefix").orEmpty()
DefaultAuthorizationService(get(), get(), get(), get(), get(), keycloakGroupPrefix)
}

singleOf(::MaintenanceService)
single { OrchestratorService(get(), get(), get()) }
single { OrganizationService(get(), get(), get(), get()) }
single { ProductService(get(), get(), get(), get()) }
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/kotlin/plugins/Lifecycle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

import org.eclipse.apoapsis.ortserver.services.AuthorizationService
import org.eclipse.apoapsis.ortserver.services.maintenance.MaintenanceService
import org.eclipse.apoapsis.ortserver.services.maintenance.jobs.DeduplicatePackagesJob
import org.eclipse.apoapsis.ortserver.utils.logging.runBlocking
import org.eclipse.apoapsis.ortserver.utils.logging.withMdcContext

import org.koin.ktor.ext.get
import org.koin.ktor.ext.inject

import org.slf4j.MDC
Expand All @@ -41,6 +44,7 @@ import org.slf4j.MDC
fun Application.configureLifecycle() {
environment.monitor.subscribe(DatabaseReady) {
val authorizationService by inject<AuthorizationService>()
val maintenanceService by inject<MaintenanceService>()

val mdcContext = MDC.getCopyOfContextMap()

Expand All @@ -50,6 +54,14 @@ fun Application.configureLifecycle() {
syncRoles(authorizationService)
}
}

thread {
MDC.setContextMap(mdcContext)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Will this be removed again when the job is done?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's stored in a ThreadLocal so it will die with the thread.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what I meant was the whole block to add the job to the maintenance service. When the migration is complete it is not needed any more. If the job is started, it will probably still do some (minor) background activity.
I guess, the underlying problem here is that the minimum maintenance service implementation does not provide a mechanism to add/remove jobs dynamically; therefore, starting of jobs has to be hard-coded.

runBlocking(Dispatchers.IO) {
maintenanceService.addJob(DeduplicatePackagesJob(get()))
maintenanceService.run()
}
}
}
}

Expand Down
66 changes: 66 additions & 0 deletions dao/src/main/kotlin/tables/MaintenanceJobsTable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2024 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.dao.tables

import kotlinx.serialization.json.JsonObject

import org.eclipse.apoapsis.ortserver.dao.utils.jsonb
import org.eclipse.apoapsis.ortserver.dao.utils.toDatabasePrecision
import org.eclipse.apoapsis.ortserver.model.MaintenanceJobData
import org.eclipse.apoapsis.ortserver.model.MaintenanceJobStatus

import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.LongEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp

/**
* A table that stores the state of maintenance jobs.
*/
object MaintenanceJobsTable : LongIdTable("maintenance_jobs") {
val name = text("name")
val status = enumerationByName<MaintenanceJobStatus>("status", 128)
val startedAt = timestamp("started_at").nullable()
val updatedAt = timestamp("updated_at").nullable()
val finishedAt = timestamp("finished_at").nullable()
val data = jsonb<JsonObject>("data").nullable()
}

class MaintenanceJobDao(id: EntityID<Long>) : LongEntity(id) {
companion object : LongEntityClass<MaintenanceJobDao>(MaintenanceJobsTable)

var name by MaintenanceJobsTable.name
var status by MaintenanceJobsTable.status
var startedAt by MaintenanceJobsTable.startedAt.transform({ it?.toDatabasePrecision() }, { it })
var updatedAt by MaintenanceJobsTable.updatedAt.transform({ it?.toDatabasePrecision() }, { it })
var finishedAt by MaintenanceJobsTable.finishedAt.transform({ it?.toDatabasePrecision() }, { it })
var data by MaintenanceJobsTable.data

fun mapToModel() = MaintenanceJobData(
id = id.value,
name = name,
status = status,
startedAt = startedAt,
updatedAt = updatedAt,
finishedAt = finishedAt,
data = data
)
}
3 changes: 2 additions & 1 deletion dao/src/main/kotlin/tables/runs/analyzer/PackagesTable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.LongEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and

/**
Expand Down Expand Up @@ -64,7 +65,7 @@ class PackageDao(id: EntityID<Long>) : LongEntity(id) {
(PackagesTable.homepageUrl eq pkg.homepageUrl) and
(PackagesTable.isMetadataOnly eq pkg.isMetadataOnly) and
(PackagesTable.isModified eq pkg.isModified)
}.firstOrNull {
}.orderBy(PackagesTable.id to SortOrder.ASC).firstOrNull {
it.identifier.mapToModel() == pkg.identifier &&
mapAndCompare(it.authors, pkg.authors, AuthorDao::name) &&
mapAndCompare(it.declaredLicenses, pkg.declaredLicenses, DeclaredLicenseDao::name) &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX packages_authors_package_id ON packages_authors(package_id);
10 changes: 10 additions & 0 deletions dao/src/main/resources/db/migration/V73__maintenanceJobs.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE maintenance_jobs
(
id bigserial PRIMARY KEY,
name text NOT NULL,
status text NOT NULL,
started_at timestamp,
updated_at timestamp,
finished_at timestamp,
data jsonb
);
File renamed without changes.
49 changes: 49 additions & 0 deletions model/src/commonMain/kotlin/MaintenanceJobData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2024 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.model

import kotlinx.datetime.Instant
import kotlinx.serialization.json.JsonObject

/**
* The data of a maintenance job, as stored in the database.
*/
data class MaintenanceJobData(
/** The unique identifier. */
val id: Long,

/** The name of the job. */
val name: String,

/** The status of the job. */
val status: MaintenanceJobStatus,

/** The time the job was started. */
val startedAt: Instant?,

/** The time the job was last updated. */
val updatedAt: Instant?,

/** The time the job finished. */
val finishedAt: Instant?,

/** The job data. */
val data: JsonObject?
)
38 changes: 38 additions & 0 deletions model/src/commonMain/kotlin/MaintenanceJobStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2024 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.model

/**
* An enum representing the status of a maintenance job.
*/
enum class MaintenanceJobStatus(val completed: Boolean) {
/** The job is currently running. */
STARTED(completed = false),

/** The job has finished successfully. */
FINISHED(completed = true),

/** The job has failed. */
FAILED(completed = true);

companion object {
val uncompletedStates = entries.filter { !it.completed }
}
}
47 changes: 47 additions & 0 deletions services/maintenance/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2024 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

plugins {
// Apply precompiled plugins.
id("ort-server-kotlin-jvm-conventions")
id("ort-server-publication-conventions")

// Apply third-party plugins.
alias(libs.plugins.kotlinSerialization)
}

group = "org.eclipse.apoapsis.ortserver.services"

dependencies {
api(projects.model)

implementation(projects.dao)
implementation(projects.utils.logging)

implementation(libs.kotlinxSerializationJson)

runtimeOnly(libs.logback)

testImplementation(testFixtures(projects.dao))
testImplementation(projects.utils.test)

testImplementation(libs.kotestAssertionsCore)
testImplementation(libs.kotestRunnerJunit5)
testImplementation(libs.mockk)
}
47 changes: 47 additions & 0 deletions services/maintenance/src/main/kotlin/MaintenanceJob.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2024 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.services.maintenance

import java.util.concurrent.atomic.AtomicBoolean

import org.eclipse.apoapsis.ortserver.model.MaintenanceJobData
import org.eclipse.apoapsis.ortserver.utils.logging.withMdcContext

abstract class MaintenanceJob {
abstract val name: String

private val _active = AtomicBoolean(false)

val active get() = _active.get()

suspend fun start(service: MaintenanceService, jobData: MaintenanceJobData) {
_active.set(true)

try {
withMdcContext("maintenanceJob" to name) {
execute(service, jobData)
}
} finally {
_active.set(false)
}
}

abstract suspend fun execute(service: MaintenanceService, jobData: MaintenanceJobData)
}
Loading