Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -48,7 +49,7 @@
* Fallback: polls every 60s to catch missed completions.
*/
@Service
public class ExtensionScanCompletionService {
public class ExtensionScanCompletionService implements JobRequestHandler<HandlerJobRequest<?>> {

protected final Logger logger = LoggerFactory.getLogger(ExtensionScanCompletionService.class);

Expand Down Expand Up @@ -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)");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,14 +42,13 @@
* Handles scan job recovery: startup recovery and runtime watchdog.
*/
@Service
public class ExtensionScanJobRecoveryService {
public class ExtensionScanJobRecoveryService implements JobRequestHandler<HandlerJobRequest<?>> {

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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<HandlerJobRequest<?>> {

private static final Logger logger = LoggerFactory.getLogger(GitleaksRulesService.class);

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -42,7 +41,7 @@
* 5. Enqueue a ScannerInvocationRequest so a worker picks it up
*/
@Service
public class ScannerConcurrencyDispatcher {
public class ScannerConcurrencyDispatcher implements JobRequestHandler<HandlerJobRequest<?>> {

private static final Logger logger = LoggerFactory.getLogger(ScannerConcurrencyDispatcher.class);

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down