diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanCompletionService.java b/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanCompletionService.java index 1b5d09fda..738754c7d 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanCompletionService.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanCompletionService.java @@ -20,6 +20,7 @@ import org.eclipse.openvsx.entities.ScanStatus; import org.eclipse.openvsx.entities.ScanCheckResult; import org.eclipse.openvsx.entities.ExtensionThreat; +import org.eclipse.openvsx.migration.HandlerJobRequest; import org.eclipse.openvsx.publish.PublishExtensionVersionService; import org.eclipse.openvsx.repositories.ExtensionScanRepository; import org.eclipse.openvsx.repositories.RepositoryService; @@ -28,7 +29,7 @@ import org.eclipse.openvsx.util.NamingUtil; import org.eclipse.openvsx.util.TimeUtil; import org.jobrunr.jobs.annotations.Job; -import org.jobrunr.jobs.annotations.Recurring; +import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -48,7 +49,7 @@ * Fallback: polls every 60s to catch missed completions. */ @Service -public class ExtensionScanCompletionService { +public class ExtensionScanCompletionService implements JobRequestHandler> { protected final Logger logger = LoggerFactory.getLogger(ExtensionScanCompletionService.class); @@ -170,8 +171,7 @@ public void checkSingleScanCompletion(String scanId) { * 3. If complete, aggregate results and activate or quarantine */ @Job(name = "Process completed scans", retries = 0) - @Recurring(id = "process-completed-scans", interval = "PT5M") - public void processCompletedScans() { + public void run(HandlerJobRequest jobRequest) throws Exception { try { logger.debug("Starting scan completion check cycle (fallback)"); diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanJobRecoveryService.java b/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanJobRecoveryService.java index baae2d82c..0aabe8298 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanJobRecoveryService.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanJobRecoveryService.java @@ -18,12 +18,13 @@ import org.eclipse.openvsx.entities.ScanCheckResult; import org.eclipse.openvsx.entities.ScannerJob; import org.eclipse.openvsx.entities.ScanStatus; +import org.eclipse.openvsx.migration.HandlerJobRequest; import org.eclipse.openvsx.publish.PublishExtensionVersionService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.repositories.ScannerJobRepository; import org.eclipse.openvsx.util.TimeUtil; import org.jobrunr.jobs.annotations.Job; -import org.jobrunr.jobs.annotations.Recurring; +import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.jobrunr.scheduling.JobRequestScheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,14 +42,13 @@ * Handles scan job recovery: startup recovery and runtime watchdog. */ @Service -public class ExtensionScanJobRecoveryService { +public class ExtensionScanJobRecoveryService implements JobRequestHandler> { private static final Logger logger = LoggerFactory.getLogger(ExtensionScanJobRecoveryService.class); // Max jobs to process per watchdog cycle private static final int MAX_PER_CYCLE = 20; - private final ScannerJobRepository scanJobRepository; private final ScannerRegistry scannerRegistry; private final RepositoryService repositories; @@ -90,6 +90,10 @@ public ExtensionScanJobRecoveryService( @EventListener(ApplicationReadyEvent.class) @Transactional public void recoverOnStartup() { + if (!scanService.isEnabled()) { + return; + } + logger.info("Starting scan recovery on server startup"); recoverPendingJobs(); @@ -435,9 +439,8 @@ private void recoverStaleScans() { * Monitor stuck jobs and timeouts. */ @Job(name = "Scan job watchdog", retries = 0) - @Recurring(id = "scan-job-watchdog", interval = "PT10M") @Transactional - public void runWatchdog() { + public void run(HandlerJobRequest jobRequest) throws Exception { recoverStuckQueuedJobs(); checkTimeouts(); } diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/GitleaksRulesService.java b/server/src/main/java/org/eclipse/openvsx/scanning/GitleaksRulesService.java index 9c3eee43f..ffaa8ca03 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/GitleaksRulesService.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/GitleaksRulesService.java @@ -16,8 +16,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.toml.TomlMapper; import io.micrometer.core.instrument.util.NamedThreadFactory; +import org.eclipse.openvsx.migration.HandlerJobRequest; import org.jobrunr.jobs.annotations.Job; -import org.jobrunr.jobs.annotations.Recurring; +import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.ObjectProvider; @@ -41,7 +42,6 @@ import java.util.List; import java.util.Set; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -59,7 +59,7 @@ */ @Service @ConditionalOnProperty(name = "ovsx.scanning.secret-detection.gitleaks.auto-fetch", havingValue = "true") -public class GitleaksRulesService extends JedisPubSub { +public class GitleaksRulesService extends JedisPubSub implements JobRequestHandler> { private static final Logger logger = LoggerFactory.getLogger(GitleaksRulesService.class); @@ -195,12 +195,7 @@ public boolean refreshRules() { * Scheduled refresh job - only runs if scheduled-refresh is enabled. */ @Job(name = "Refresh gitleaks rules", retries = 0) - @Recurring( - id = "refresh-gitleaks-rules", - cron = "0 0 3 * * *", - zoneId = "UTC" - ) - public void scheduledRefresh() { + public void run(HandlerJobRequest jobRequest) throws Exception { // Check if scheduled refresh is enabled at runtime if (!config.isGitleaksScheduledRefresh()) { return; diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/ScannerConcurrencyDispatcher.java b/server/src/main/java/org/eclipse/openvsx/scanning/ScannerConcurrencyDispatcher.java index e90d73d4a..ae900a2e9 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/ScannerConcurrencyDispatcher.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/ScannerConcurrencyDispatcher.java @@ -13,18 +13,17 @@ package org.eclipse.openvsx.scanning; import org.eclipse.openvsx.entities.ScannerJob; +import org.eclipse.openvsx.migration.HandlerJobRequest; import org.eclipse.openvsx.repositories.ScannerJobRepository; import org.eclipse.openvsx.util.TimeUtil; import org.jobrunr.jobs.annotations.Job; -import org.jobrunr.jobs.annotations.Recurring; +import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.jobrunr.scheduling.JobRequestScheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; import java.util.List; /** @@ -42,7 +41,7 @@ * 5. Enqueue a ScannerInvocationRequest so a worker picks it up */ @Service -public class ScannerConcurrencyDispatcher { +public class ScannerConcurrencyDispatcher implements JobRequestHandler> { private static final Logger logger = LoggerFactory.getLogger(ScannerConcurrencyDispatcher.class); @@ -65,8 +64,7 @@ public ScannerConcurrencyDispatcher( * Only one instance of this recurring job runs across all pods at a time. */ @Job(name = "Scanner concurrency dispatcher", retries = 0) - @Recurring(id = "scanner-concurrency-dispatcher", interval = "PT15S") - public void dispatch() { + public void run(HandlerJobRequest jobRequest) throws Exception { boolean anyLimited = scannerRegistry.getAllScanners().stream() .anyMatch(s -> s.getMaxConcurrency() > 0); if (!anyLimited) { diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/ScheduleScanningJobs.java b/server/src/main/java/org/eclipse/openvsx/scanning/ScheduleScanningJobs.java new file mode 100644 index 000000000..f5ac7bbd5 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/scanning/ScheduleScanningJobs.java @@ -0,0 +1,121 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ +package org.eclipse.openvsx.scanning; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.jobrunr.scheduling.JobRequestScheduler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.ZoneOffset; + +@Component +public class ScheduleScanningJobs { + private final Logger logger = LoggerFactory.getLogger(ScheduleScanningJobs.class); + + private static final String SCHEDULE_SCAN_WATCHDOG_JOB = "scan-job-watchdog"; + private static final String SCHEDULE_SCAN_COMPLETION_JOB = "process-completed-scans"; + private static final String SCHEDULE_SCAN_CONCURRENCY_DISPATCHER_JOB = "scanner-concurrency-dispatcher"; + private static final String SCHEDULE_GITLEAKS_RULES_REFRESH_JOB = "refresh-gitleaks-rules"; + + private final ExtensionScanConfig scanConfig; + private final SecretDetectorConfig secretDetectorConfig; + private final RemoteScannerProperties remoteScannerProperties; + private final JobRequestScheduler scheduler; + + public ScheduleScanningJobs( + ExtensionScanService scanService, + ExtensionScanConfig scanConfig, + SecretDetectorConfig secretDetectorConfig, + RemoteScannerProperties remoteScannerProperties, + JobRequestScheduler scheduler + ) { + this.scanConfig = scanConfig; + this.secretDetectorConfig = secretDetectorConfig; + this.remoteScannerProperties = remoteScannerProperties; + this.scheduler = scheduler; + } + + @EventListener + public void scheduleJobs(ApplicationStartedEvent event) { + var enabledScanners = + remoteScannerProperties + .getScanners() + .values() + .stream() + .filter(RemoteScannerProperties.ScannerConfig::isEnabled).toList(); + + // schedule the scan recovery watchdog when there are remote scanners enabled + if (scanConfig.isEnabled() && !enabledScanners.isEmpty()) { + var interval = Duration.parse("PT10M"); + logger.info("Scheduling scan recovery job with interval '{}'", interval); + + scheduler.scheduleRecurrently( + SCHEDULE_SCAN_WATCHDOG_JOB, + interval, + new HandlerJobRequest<>(ExtensionScanJobRecoveryService.class) + ); + } else { + scheduler.deleteRecurringJob(SCHEDULE_SCAN_WATCHDOG_JOB); + } + + // schedule the scan completion job when remote scanners are enabled + if (scanConfig.isEnabled() && !enabledScanners.isEmpty()) { + var interval = Duration.parse("PT5M"); + logger.info("Scheduling scan completion job with interval '{}'", interval); + + scheduler.scheduleRecurrently( + SCHEDULE_SCAN_COMPLETION_JOB, + interval, + new HandlerJobRequest<>(ExtensionScanCompletionService.class) + ); + } else { + scheduler.deleteRecurringJob(SCHEDULE_SCAN_COMPLETION_JOB); + } + + var enabledScannersWithConcurrency = enabledScanners.stream().anyMatch(c -> c.getMaxConcurrency() > 0); + + // schedule scan concurrency dispatcher if there are enabled scanners with a maxConcurrency setting > 0 + if (scanConfig.isEnabled() && enabledScannersWithConcurrency) { + var interval = Duration.parse("PT15S"); + logger.info("Scheduling scan concurrency dispatcher job with interval '{}'", interval); + + scheduler.scheduleRecurrently( + SCHEDULE_SCAN_CONCURRENCY_DISPATCHER_JOB, + interval, + new HandlerJobRequest<>(ScannerConcurrencyDispatcher.class) + ); + } else { + scheduler.deleteRecurringJob(SCHEDULE_SCAN_CONCURRENCY_DISPATCHER_JOB); + } + + // schedule the GitLeaks rules refresh job if enabled + if (scanConfig.isEnabled() && secretDetectorConfig.isEnabled() && secretDetectorConfig.isGitleaksScheduledRefresh()) { + var schedule = secretDetectorConfig.getGitleaksRefreshCron(); + logger.info("Scheduling GitLeaks rules refresh job with schedule '{}'", schedule); + + scheduler.scheduleRecurrently( + SCHEDULE_GITLEAKS_RULES_REFRESH_JOB, + schedule, + ZoneOffset.UTC, + new HandlerJobRequest<>(GitleaksRulesService.class) + ); + } else { + scheduler.deleteRecurringJob(SCHEDULE_GITLEAKS_RULES_REFRESH_JOB); + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/storage/log/AzureDownloadCountHandler.java b/server/src/main/java/org/eclipse/openvsx/storage/log/AzureDownloadCountHandler.java index f11855567..be729d5eb 100644 --- a/server/src/main/java/org/eclipse/openvsx/storage/log/AzureDownloadCountHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/storage/log/AzureDownloadCountHandler.java @@ -24,7 +24,6 @@ import org.eclipse.openvsx.migration.HandlerJobRequest; import org.eclipse.openvsx.util.TempFile; import org.jobrunr.jobs.annotations.Job; -import org.jobrunr.jobs.annotations.Recurring; import org.jobrunr.jobs.lambdas.JobRequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory;