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

Implement cache store for build records #505

Merged
merged 22 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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: 1 addition & 2 deletions VERSION
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
1.7.9

1.8.0-B0
pditommaso marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ class BuildConfig {
@Value('${wave.build.reserved-words:[]}')
Set<String> reservedWords

@Value('${wave.build.record.duration:5d}')
Duration recordDuration

@PostConstruct
private void init() {
log.debug("Builder config: " +
Expand All @@ -95,6 +98,7 @@ class BuildConfig {
"status-delay=${statusDelay}; " +
"status-duration=${statusDuration}; " +
"compress-caching=$compressCaching; " +
"record-duration=${recordDuration}; " +
"cleanup=${cleanup}; ")
}

Expand Down
10 changes: 5 additions & 5 deletions src/main/groovy/io/seqera/wave/controller/BuildController.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ import io.micronaut.http.annotation.Produces
import io.micronaut.http.server.types.files.StreamedFile
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import io.seqera.wave.api.BuildStatusResponse
import io.seqera.wave.service.builder.ContainerBuildService
import io.seqera.wave.service.logs.BuildLogService
import io.seqera.wave.service.persistence.PersistenceService
import io.seqera.wave.service.persistence.WaveBuildRecord
import jakarta.inject.Inject
import io.seqera.wave.api.BuildStatusResponse
/**
* Implements a controller for container builds
*
Expand All @@ -46,15 +46,15 @@ import io.seqera.wave.api.BuildStatusResponse
class BuildController {

@Inject
private PersistenceService persistenceService
private ContainerBuildService buildService

@Inject
@Nullable
BuildLogService logService

@Get("/v1alpha1/builds/{buildId}")
HttpResponse<WaveBuildRecord> getBuildRecord(String buildId){
final record = persistenceService.loadBuild(buildId)
final record = buildService.getBuildRecord(buildId)
return record
? HttpResponse.ok(record)
: HttpResponse.<WaveBuildRecord>notFound()
Expand All @@ -73,7 +73,7 @@ class BuildController {

@Get("/v1alpha1/builds/{buildId}/status")
HttpResponse<BuildStatusResponse> getBuildStatus(String buildId){
final build = persistenceService.loadBuild(buildId)
final build = buildService.getBuildRecord(buildId)
build != null
? HttpResponse.ok(build.toStatusResponse())
: HttpResponse.<BuildStatusResponse>notFound()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import io.micronaut.views.View
import io.seqera.wave.exception.NotFoundException
import io.seqera.wave.service.builder.ContainerBuildService
import io.seqera.wave.service.logs.BuildLogService
import io.seqera.wave.service.persistence.PersistenceService
import io.seqera.wave.service.persistence.WaveBuildRecord
Expand All @@ -53,14 +54,17 @@ class ViewController {
@Inject
private PersistenceService persistenceService

@Inject
private ContainerBuildService buildService

@Inject
@Nullable
private BuildLogService buildLogService

@View("build-view")
@Get('/builds/{buildId}')
HttpResponse<Map<String,String>> viewBuild(String buildId) {
final record = persistenceService.loadBuild(buildId)
final record = buildService.getBuildRecord(buildId)
if( !record )
throw new NotFoundException("Unknown build id '$buildId'")
return HttpResponse.ok(renderBuildView(record))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ package io.seqera.wave.service.builder

import java.util.concurrent.CompletableFuture

import groovy.transform.CompileStatic
import io.micronaut.runtime.event.annotation.EventListener
import io.seqera.wave.core.RoutePath
import io.seqera.wave.service.persistence.WaveBuildRecord

/**
* Declare container build service interface
*
* @author Paolo Di Tommaso <[email protected]>
*/
@CompileStatic
interface ContainerBuildService {

/**
Expand Down Expand Up @@ -65,4 +70,66 @@ interface ContainerBuildService {
: null
}


// **************************************************************
// ** build record operations
// **************************************************************

@EventListener
default void onBuildEvent(BuildEvent event) {
saveBuildRecord(event)
}

/**
* Store a build record for the given {@link BuildRequest} object.
*
* This method is expected to store the build record associated with the request
* *only* in the short term store caching system, ie. without hitting the
* long-term SurrealDB storage
*
* @param request The build request that needs to be storage
*/
default void createBuildRecord(BuildRequest request) {
final record0 = WaveBuildRecord.fromEvent(new BuildEvent(request))
createBuildRecord(record0.buildId, record0)
}

/**
* Store the build record associated with the specified event both in the
* short-term cache (redis) and long-term persistence layer (surrealdb)
*
* @param event The {@link BuildEvent} object for which the build record needs to be stored
*/
default void saveBuildRecord(BuildEvent event) {
final record0 = WaveBuildRecord.fromEvent(event)
saveBuildRecord(record0.buildId, record0)
}

/**
* Store a build record object.
*
* This method is expected to store the build record *only* in the short term store cache (redis),
* ie. without hitting the long-term storage (surrealdb)
*
* @param buildId The Id of the build record
* @param value The {@link WaveBuildRecord} to be stored
*/
void createBuildRecord(String buildId, WaveBuildRecord value)

/**
* Store the specified build record both in the short-term cache (redis)
* and long-term persistence layer (surrealdb)
*
* @param buildId The Id of the build record
* @param value The {@link WaveBuildRecord} to be stored
*/
void saveBuildRecord(String buildId, WaveBuildRecord value)

/**
* Retrieve the build record for the specified id.
*
* @param buildId The ID of the build record to be retrieve
* @return The {@link WaveBuildRecord} associated with the corresponding Id, or {@code null} if it cannot be found
*/
WaveBuildRecord getBuildRecord(String buildId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import io.seqera.wave.configuration.SpackConfig
import io.seqera.wave.exception.HttpServerRetryableErrorException
import io.seqera.wave.ratelimit.AcquireRequest
import io.seqera.wave.ratelimit.RateLimiterService
import io.seqera.wave.service.builder.store.BuildRecordStore
import io.seqera.wave.service.cleanup.CleanupStrategy
import io.seqera.wave.service.metric.MetricsService
import io.seqera.wave.service.persistence.PersistenceService
Expand Down Expand Up @@ -115,6 +116,9 @@ class ContainerBuildServiceImpl implements ContainerBuildService {
@Inject
private MetricsService metricsService

@Inject
BuildRecordStore buildRecordStore

/**
* Build a container image for the given {@link BuildRequest}
*
Expand Down Expand Up @@ -237,8 +241,8 @@ class ContainerBuildServiceImpl implements ContainerBuildService {
//increment metrics
CompletableFuture.supplyAsync(() -> metricsService.incrementBuildsCounter(request.identity), executor)

// persist the container request
persistenceService.createBuild(WaveBuildRecord.fromEvent(new BuildEvent(request)))
// save the container request in the underlying storage (redis)
createBuildRecord(request)

// launch the build async
CompletableFuture
Expand Down Expand Up @@ -341,4 +345,34 @@ class ContainerBuildServiceImpl implements ContainerBuildService {
.retryCondition((Throwable t) -> t instanceof SocketException || t instanceof HttpServerRetryableErrorException)
.onRetry((event)-> log.warn("$message - event: $event"))
}

// **************************************************************
// ** build record implementation
// **************************************************************

/**
* @Inherited
*/
@Override
void createBuildRecord(String buildId, WaveBuildRecord value) {
buildRecordStore.putBuildRecord(buildId, value)
}

/**
* @Inherited
*/
@Override
void saveBuildRecord(String buildId, WaveBuildRecord value) {
buildRecordStore.putBuildRecord(buildId, value)
persistenceService.saveBuild(value)
}

/**
* @Inherited
*/
@Override
WaveBuildRecord getBuildRecord(String buildId) {
return buildRecordStore.getBuildRecord(buildId) ?: persistenceService.loadBuild(buildId)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2024, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.service.builder.store

import java.time.Duration

import groovy.transform.CompileStatic
import io.seqera.wave.configuration.BuildConfig
import io.seqera.wave.encoder.MoshiEncodeStrategy
import io.seqera.wave.service.cache.AbstractCacheStore
import io.seqera.wave.service.cache.impl.CacheProvider
import io.seqera.wave.service.persistence.WaveBuildRecord
import jakarta.inject.Inject
import jakarta.inject.Singleton
/**
* Implements a store service to cache {@link WaveBuildRecord} object
*
* @author Paolo Di Tommaso <[email protected]>
*/
@Singleton
@CompileStatic
class BuildRecordCacheStore extends AbstractCacheStore<WaveBuildRecord> implements BuildRecordStore {

/**
* An instance of {@link BuildConfig} modelling the build config settings
*/
@Inject
private BuildConfig buildConfig

BuildRecordCacheStore(CacheProvider<String, String> provider) {
super(provider, new MoshiEncodeStrategy<WaveBuildRecord>() {})
}

@Override
protected String getPrefix() {
return 'wave-buildrecord/v1:'
}

@Override
protected Duration getDuration() {
return buildConfig.recordDuration
}

@Override
WaveBuildRecord getBuildRecord(String buildId) {
return get(buildId)
}

@Override
void putBuildRecord(String buildId, WaveBuildRecord value) {
put(buildId, value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2024, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.service.builder.store

import io.seqera.wave.service.persistence.WaveBuildRecord

/**
* Define the contract to cache {@link WaveBuildRecord} object
*
* @author Paolo Di Tommaso <[email protected]>
*/
interface BuildRecordStore {

WaveBuildRecord getBuildRecord(String buildId)

void putBuildRecord(String buildId, WaveBuildRecord value)

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@
package io.seqera.wave.service.persistence

import groovy.transform.CompileStatic
import io.micronaut.runtime.event.annotation.EventListener
import io.seqera.wave.core.ContainerDigestPair
import io.seqera.wave.exception.NotFoundException
import io.seqera.wave.service.scan.ScanResult
import io.seqera.wave.service.builder.BuildEvent

/**
* A storage for statistic data
*
Expand All @@ -34,11 +31,6 @@ import io.seqera.wave.service.builder.BuildEvent
@CompileStatic
interface PersistenceService {

@EventListener
default void onBuildEvent(BuildEvent event) {
updateBuild(WaveBuildRecord.fromEvent(event))
}

/**
* Store a {@link WaveBuildRecord} object in the underlying persistence layer.
*
Expand All @@ -47,17 +39,7 @@ interface PersistenceService {
*
* @param build A {@link WaveBuildRecord} object
*/
void createBuild(WaveBuildRecord build)

/**
* Update the build record. This function is expect to update only the following fields
* - digest
* - duration
* - exitStatus
*
* @param build The build record to be updated
*/
void updateBuild(WaveBuildRecord build)
void saveBuild(WaveBuildRecord build)

/**
* Retrieve a {@link WaveBuildRecord} object for the given build id
Expand Down
Loading