From c742f7735b9e55a4d443b59589d6b0c8ec51f012 Mon Sep 17 00:00:00 2001 From: Adrika Gupta Date: Thu, 18 Apr 2024 14:59:12 +0530 Subject: [PATCH] added implementation for periodic refresh of soure control metadata --- .../java/io/cdap/cdap/app/store/Store.java | 2 +- .../io/cdap/cdap/app/store/SyncStatus.java | 26 +++ .../SourceControlManagementHttpHandler.java | 2 - .../SourceControlManagementService.java | 67 +++--- .../SourceControlMetadataRefreshService.java | 197 ++++++++++++++++++ .../cdap/internal/app/store/DefaultStore.java | 9 +- .../SourceControlManagementServiceTest.java | 5 +- ...urceControlMetadataRefreshServiceTest.java | 85 ++++++++ ...urceControlManagementHttpHandlerTests.java | 6 +- .../proto/SourceControlMetadataDetail.java | 62 ++++++ 10 files changed, 424 insertions(+), 37 deletions(-) create mode 100644 cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/SyncStatus.java create mode 100644 cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SourceControlMetadataRefreshService.java create mode 100644 cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SourceControlMetadataRefreshServiceTest.java create mode 100644 cdap-proto/src/main/java/io/cdap/cdap/proto/SourceControlMetadataDetail.java diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/Store.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/Store.java index 8c470730e79a..21828fa838ed 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/Store.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/Store.java @@ -495,7 +495,7 @@ int scanAppSourceControlMetadata(ScanSourceControlMetadataRequest request, int scanRepositorySourceControlMetadata(ScanSourceControlMetadataRequest request, Consumer consumer); - void updateSourceControlMeta(ApplicationReference appRef, String repoFileHash); + void updateSourceControlMeta(ApplicationReference appRef, String repoFileHash, Long lastRefreshTime); /** * Returns a Map of {@link ApplicationMeta} for the given set of {@link ApplicationId}. diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/SyncStatus.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/SyncStatus.java new file mode 100644 index 000000000000..5ca8054d55df --- /dev/null +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/SyncStatus.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * 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 + * + * http://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. + */ + +package io.cdap.cdap.app.store; + +/** + * Represents the sync status of source control metadata. + * It is used in filtering. + */ +public enum SyncStatus { + SYNCED, + UNSYNCED +} diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/SourceControlManagementHttpHandler.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/SourceControlManagementHttpHandler.java index 99801e766383..8bee5470296e 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/SourceControlManagementHttpHandler.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/SourceControlManagementHttpHandler.java @@ -171,8 +171,6 @@ record -> { }); } catch (IOException e) { responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } catch (NotFoundException e) { - responder.sendString(HttpResponseStatus.NOT_FOUND, e.getMessage()); } SourceControlMetadataRecord record = lastRecord.get(); return !pageLimitReached || record == null ? null : diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SourceControlManagementService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SourceControlManagementService.java index c2dd4e3b1c39..ae493f231078 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SourceControlManagementService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SourceControlManagementService.java @@ -98,6 +98,7 @@ public class SourceControlManagementService { private final TransactionRunner transactionRunner; private final CConfiguration cConf; private final SecureStore secureStore; + private final SourceControlMetadataRefreshService sourceControlMetadataRefreshService; private final SourceControlOperationRunner sourceControlOperationRunner; private final ApplicationLifecycleService appLifecycleService; private final Store store; @@ -120,11 +121,13 @@ public SourceControlManagementService(CConfiguration cConf, ApplicationLifecycleService applicationLifecycleService, Store store, OperationLifecycleManager operationLifecycleManager, - MetricsCollectionService metricsCollectionService) { + MetricsCollectionService metricsCollectionService, + SourceControlMetadataRefreshService sourceControlMetadataRefreshService) { this (cConf, secureStore, transactionRunner, accessEnforcer, authenticationContext, sourceControlOperationRunner, applicationLifecycleService, - store, operationLifecycleManager, metricsCollectionService, Clock.systemUTC()); + store, operationLifecycleManager, metricsCollectionService, Clock.systemUTC(), + sourceControlMetadataRefreshService); } @VisibleForTesting @@ -138,7 +141,7 @@ public SourceControlManagementService(CConfiguration cConf, Store store, OperationLifecycleManager operationLifecycleManager, MetricsCollectionService metricsCollectionService, - Clock clock) { + Clock clock, SourceControlMetadataRefreshService sourceControlMetadataRefreshService) { this.cConf = cConf; this.secureStore = secureStore; this.transactionRunner = transactionRunner; @@ -150,6 +153,7 @@ public SourceControlManagementService(CConfiguration cConf, this.operationLifecycleManager = operationLifecycleManager; this.metricsCollectionService = metricsCollectionService; this.clock = clock; + this.sourceControlMetadataRefreshService = sourceControlMetadataRefreshService; } private RepositoryTable getRepositoryTable(StructuredTableContext context) @@ -188,6 +192,7 @@ public RepositoryMeta setRepository(NamespaceId namespace, RepositoryConfig repo RepositoryTable repoTable = getRepositoryTable(context); repoTable.create(namespace, repository); + sourceControlMetadataRefreshService.runRefreshService(true, namespace); return repoTable.get(namespace); }, NamespaceNotFoundException.class); } @@ -397,33 +402,32 @@ private PullAppResponse pullAndValidateApplication(ApplicationReference appRe * @throws IOException If an I/O error occurs during the scanning process. */ public boolean scanRepoMetadata(ScanSourceControlMetadataRequest request, int txBatchSize, - Consumer consumer) throws NotFoundException, - AuthenticationConfigException, IOException { + Consumer consumer) throws IOException { NamespaceId namespaceId = new NamespaceId(request.getNamespace()); - accessEnforcer.enforce(namespaceId, authenticationContext.getPrincipal(), - NamespacePermission.READ_REPOSITORY); - RepositoryConfig repoConfig = getRepositoryMeta(namespaceId).getConfig(); - // TODO(CDAP-20993): List API is used here for testing. It will be moved to a separate background job in the next PR - RepositoryAppsResponse repositoryAppsResponse = sourceControlOperationRunner.list( - new NamespaceRepository(namespaceId, repoConfig)); - LOG.debug("Successfully received apps in namespace {} from repository : response: {}", - namespaceId, - repositoryAppsResponse); - // Cleaning up the repo source control metadata table - HashSet repoFileNames = new HashSet<>(); - for (RepositoryApp repoApp : repositoryAppsResponse.getApps()) { - repoFileNames.add(repoApp.getName()); - } - TransactionRunners.run(transactionRunner, context -> { - getRepoSourceControlMetadataStore(context).cleanupRepoSourceControlMeta( - namespaceId.getNamespace(), repoFileNames); - }); - // Updating the namespace and repo source control metadata table - for (RepositoryApp repoApp : repositoryAppsResponse.getApps()) { - store.updateSourceControlMeta( - new ApplicationReference(request.getNamespace(), - repoApp.getName()), repoApp.getFileHash()); - } +// accessEnforcer.enforce(namespaceId, authenticationContext.getPrincipal(), +// NamespacePermission.READ_REPOSITORY); +// RepositoryConfig repoConfig = getRepositoryMeta(namespaceId).getConfig(); +// // TODO(CDAP-20993): List API is used here for testing. It will be moved to a separate background job in the next PR +// RepositoryAppsResponse repositoryAppsResponse = sourceControlOperationRunner.list( +// new NamespaceRepository(namespaceId, repoConfig)); +// LOG.debug("Successfully received apps in namespace {} from repository : response: {}", +// namespaceId, +// repositoryAppsResponse); +// // Cleaning up the repo source control metadata table +// HashSet repoFileNames = new HashSet<>(); +// for (RepositoryApp repoApp : repositoryAppsResponse.getApps()) { +// repoFileNames.add(repoApp.getName()); +// } +// TransactionRunners.run(transactionRunner, context -> { +// getRepoSourceControlMetadataStore(context).cleanupRepoSourceControlMeta( +// namespaceId.getNamespace(), repoFileNames); +// }); +// // Updating the namespace and repo source control metadata table +// for (RepositoryApp repoApp : repositoryAppsResponse.getApps()) { +// store.updateSourceControlMeta( +// new ApplicationReference(request.getNamespace(), +// repoApp.getName()), repoApp.getFileHash()); +// } // Getting repo files String lastKey = request.getScanAfter(); int currentLimit = request.getLimit(); @@ -447,6 +451,11 @@ public boolean scanRepoMetadata(ScanSourceControlMetadataRequest request, int tx } currentLimit -= txBatchSize; } + try { + sourceControlMetadataRefreshService.runRefreshService(false, namespaceId); + } catch (Exception e){ + // log it + } return true; } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SourceControlMetadataRefreshService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SourceControlMetadataRefreshService.java new file mode 100644 index 000000000000..a1fb8b3e67bd --- /dev/null +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SourceControlMetadataRefreshService.java @@ -0,0 +1,197 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * 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 + * + * http://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. + */ + +package io.cdap.cdap.internal.app.services; + +import com.google.common.util.concurrent.AbstractScheduledService; +import io.cdap.cdap.app.store.Store; +import io.cdap.cdap.common.NotFoundException; +import io.cdap.cdap.common.RepositoryNotFoundException; +import io.cdap.cdap.common.namespace.NamespaceAdmin; +import io.cdap.cdap.internal.app.store.RepositorySourceControlMetadataStore; +import io.cdap.cdap.proto.id.ApplicationReference; +import io.cdap.cdap.proto.id.NamespaceId; +import io.cdap.cdap.proto.security.NamespacePermission; +import io.cdap.cdap.proto.security.StandardPermission; +import io.cdap.cdap.proto.sourcecontrol.RepositoryConfig; +import io.cdap.cdap.proto.sourcecontrol.RepositoryMeta; +import io.cdap.cdap.security.spi.authentication.AuthenticationContext; +import io.cdap.cdap.security.spi.authorization.AccessEnforcer; +import io.cdap.cdap.sourcecontrol.operationrunner.NamespaceRepository; +import io.cdap.cdap.sourcecontrol.operationrunner.RepositoryApp; +import io.cdap.cdap.sourcecontrol.operationrunner.RepositoryAppsResponse; +import io.cdap.cdap.sourcecontrol.operationrunner.SourceControlOperationRunner; +import io.cdap.cdap.spi.data.StructuredTableContext; +import io.cdap.cdap.spi.data.TableNotFoundException; +import io.cdap.cdap.spi.data.transaction.TransactionRunner; +import io.cdap.cdap.spi.data.transaction.TransactionRunners; +import io.cdap.cdap.store.RepositoryTable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.apache.twill.common.Threads; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SourceControlMetadataRefreshService extends AbstractScheduledService { + + private List namespaces; + private HashMap lastRefreshTimes; + + private final AccessEnforcer accessEnforcer; + private final AuthenticationContext authenticationContext; + + private final NamespaceAdmin namespaceAdmin; + private ScheduledExecutorService executor; + private final TransactionRunner transactionRunner; + private final SourceControlOperationRunner sourceControlOperationRunner; + private final Store store; + private static final Logger LOG = LoggerFactory.getLogger( + SourceControlMetadataRefreshService.class); + + public SourceControlMetadataRefreshService(AccessEnforcer accessEnforcer, + AuthenticationContext authenticationContext, NamespaceAdmin namespaceAdmin, + TransactionRunner transactionRunner, + SourceControlOperationRunner sourceControlOperationRunner, Store store) { + this.accessEnforcer = accessEnforcer; + this.authenticationContext = authenticationContext; + this.namespaceAdmin = namespaceAdmin; + this.transactionRunner = transactionRunner; + this.sourceControlOperationRunner = sourceControlOperationRunner; + this.store = store; + } + + private void setNamespaces(List namespaceIds) { + this.namespaces = namespaceIds; + } + + private RepositorySourceControlMetadataStore getRepoSourceControlMetadataStore( + StructuredTableContext context) { + return RepositorySourceControlMetadataStore.create(context); + } + + @Override + protected final ScheduledExecutorService executor() { + executor = Executors.newSingleThreadScheduledExecutor( + Threads.createDaemonThreadFactory("source-control-metadata-refresh-service")); + return executor; + } + + private RepositoryTable getRepositoryTable(StructuredTableContext context) + throws TableNotFoundException { + return new RepositoryTable(context); + } + + + public RepositoryMeta getRepositoryMeta(NamespaceId namespace) + throws RepositoryNotFoundException { + accessEnforcer.enforce(namespace, authenticationContext.getPrincipal(), StandardPermission.GET); + + return TransactionRunners.run(transactionRunner, context -> { + RepositoryTable table = getRepositoryTable(context); + RepositoryMeta repoMeta = table.get(namespace); + if (repoMeta == null) { + throw new RepositoryNotFoundException(namespace); + } + + return repoMeta; + }, RepositoryNotFoundException.class); + } + + @Override + protected void runOneIteration() { + try { + if (namespaces == null) { + setNamespaces(namespaceAdmin.list().stream() + .map(meta -> meta.getNamespaceId()).collect(Collectors.toList())); + + } + + for (NamespaceId namespaceId : namespaces) { + RepositoryConfig repoConfig = getRepositoryMeta(namespaceId).getConfig(); + + accessEnforcer.enforce(namespaceId, authenticationContext.getPrincipal(), + NamespacePermission.READ_REPOSITORY); + RepositoryAppsResponse repositoryAppsResponse = sourceControlOperationRunner.list( + new NamespaceRepository(namespaceId, repoConfig)); + + // Cleaning up the repo source control metadata table + HashSet repoFileNames = new HashSet<>(); + for (RepositoryApp repoApp : repositoryAppsResponse.getApps()) { + repoFileNames.add(repoApp.getName()); + } + TransactionRunners.run(transactionRunner, context -> { + getRepoSourceControlMetadataStore(context).cleanupRepoSourceControlMeta( + namespaceId.getNamespace(), repoFileNames); + }); + // Updating the namespace and repo source control metadata table + for (RepositoryApp repoApp : repositoryAppsResponse.getApps()) { + store.updateSourceControlMeta( + new ApplicationReference(namespaceId.getNamespace(), repoApp.getName()), + repoApp.getFileHash(), + lastRefreshTimes.get(namespaceId)); + } + lastRefreshTimes.put(namespaceId, System.currentTimeMillis()); + } + + setNamespaces(null); + } catch (RepositoryNotFoundException e) { + // LOG + } catch (NotFoundException e) { + // LOG + } catch (Exception e) { + // LOG + } + + } + + public void runRefreshService(boolean forced, NamespaceId namespace) throws Exception { + if (!forced && (System.currentTimeMillis() - lastRefreshTimes.get(namespace) + < 10 * 60 * 1000)) { + return; + } + List namespaceList = new ArrayList<>(); + namespaceList.add(namespace); + setNamespaces(namespaceList); + runOneIteration(); + } + + @Override + protected void startUp() throws Exception { + this.namespaces = namespaceAdmin.list().stream() + .map(meta -> meta.getNamespaceId()).collect(Collectors.toList()); + namespaces.forEach(n -> lastRefreshTimes.put(n, 0L)); + } + + @Override + protected Scheduler scheduler() { + return Scheduler.newFixedRateSchedule(1, 60 * 60, TimeUnit.SECONDS); + } + + @Override + protected void shutDown() throws Exception { + if (executor != null) { + executor.shutdownNow(); + } + } +} diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/DefaultStore.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/DefaultStore.java index 473de3a2a1cc..c0430669bf90 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/DefaultStore.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/DefaultStore.java @@ -76,6 +76,7 @@ import io.cdap.cdap.spi.data.transaction.TransactionRunners; import io.cdap.cdap.store.StoreDefinition; import java.io.IOException; +import java.time.Instant; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; @@ -993,7 +994,7 @@ public ApplicationMeta getLatest(ApplicationReference appRef) { } @Override - public void updateSourceControlMeta(ApplicationReference appRef, String repoFileHash) { + public void updateSourceControlMeta(ApplicationReference appRef, String repoFileHash, Long lastRefreshTime) { TransactionRunners.run(transactionRunner, context -> { RepositorySourceControlMetadataStore repoMetadataStore = getRepoSourceControlMetadataStore( context); @@ -1005,6 +1006,12 @@ public void updateSourceControlMeta(ApplicationReference appRef, String repoFile repoMetadataStore.write(appRef, false, 0L); return; } + if (namespaceSourceControlMeta.getLastSyncedAt() != null && namespaceSourceControlMeta.getLastSyncedAt().isAfter( + Instant.ofEpochMilli(lastRefreshTime))) { + repoMetadataStore.write(appRef, + namespaceSourceControlMeta.getSyncStatus(), namespaceSourceControlMeta.getLastSyncedAt().toEpochMilli()); + return; + } Boolean isSynced = namespaceSourceControlMeta.getFileHash().equals(repoFileHash); if (isSynced) { namespaceMetadataStore.write(appRef, diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SourceControlManagementServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SourceControlManagementServiceTest.java index 01c04ec33600..9b6b00730c5d 100644 --- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SourceControlManagementServiceTest.java +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SourceControlManagementServiceTest.java @@ -104,6 +104,7 @@ public class SourceControlManagementServiceTest extends AppFabricTestBase { private static CConfiguration cConf; private static final String TYPE = EntityType.APPLICATION.toString(); private static NamespaceAdmin namespaceAdmin; + private static SourceControlMetadataRefreshService sourceControlMetadataRefreshService; private static SourceControlManagementService sourceControlService; private static final RepositoryConfig REPOSITORY_CONFIG = new RepositoryConfig.Builder() .setProvider(Provider.GITHUB) @@ -148,13 +149,13 @@ public SourceControlManagementService provideSourceControlManagementService( SourceControlOperationRunner sourceControlRunner, ApplicationLifecycleService applicationLifecycleService, Store store, MetricsCollectionService metricsCollectionService, - OperationLifecycleManager manager) { + OperationLifecycleManager manager, SourceControlMetadataRefreshService sourceControlMetadataRefreshService) { return new SourceControlManagementService(cConf, secureStore, transactionRunner, accessEnforcer, authenticationContext, sourceControlRunner, applicationLifecycleService, store, manager, metricsCollectionService, - Clock.fixed(fixedInstant, ZoneId.systemDefault())); + Clock.fixed(fixedInstant, ZoneId.systemDefault()), sourceControlMetadataRefreshService); } }); } diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SourceControlMetadataRefreshServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SourceControlMetadataRefreshServiceTest.java new file mode 100644 index 000000000000..412fd7d8b744 --- /dev/null +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SourceControlMetadataRefreshServiceTest.java @@ -0,0 +1,85 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * 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 + * + * http://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. + */ + +package io.cdap.cdap.internal.app.services; + +import com.google.common.util.concurrent.AbstractScheduledService; +import io.cdap.cdap.common.NotFoundException; +import org.junit.Test; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class SourceControlMetadataRefreshServiceTest { + @Mock + private SourceControlMetadataRefreshService sourceControlMetadataRefreshService; + @Before + public void setUp() { + //sourceControlMetadataRefreshService = new SourceControlMetadataRefreshService(); + } + @After + public void tearDown() { + // sourceControlMetadataRefreshService.stop(); + } + @Test + public void testStartUp() throws Exception { + sourceControlMetadataRefreshService.startUp(); + verify(sourceControlMetadataRefreshService, times(1)).startUp(); + } + @Test + public void testRunOneIteration() throws NotFoundException { +// sourceControlMetadataRefreshService.startAsync().awaitRunning(); + sourceControlMetadataRefreshService.runOneIteration(); + verify(sourceControlMetadataRefreshService, times(1)).runOneIteration(); + } + + @Test + public void testDuetNegativePeriod() { + try { + SourceControlMetadataRefreshService.Scheduler.newFixedRateSchedule(1, -5, TimeUnit.SECONDS); + // If the above line does not throw an exception, fail the test + fail("Expected IllegalArgumentException was not thrown"); + } catch (IllegalArgumentException e) { + // The expected exception was thrown, so the test passes + } + } + + +// @Test(expected = IllegalArgumentException.class) +// public void testNegativePeriod() { +// SourceControlMetadataRefreshService service = new SourceControlMetadataRefreshService(); +// +// try { +// service.start(); // This should throw IllegalArgumentException +// } finally { +// // executor.shutdownNow(); +// service.stop(); +// } +// } + +} \ No newline at end of file diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/SourceControlManagementHttpHandlerTests.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/SourceControlManagementHttpHandlerTests.java index 45f51e315205..cc0ab0f7dda1 100644 --- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/SourceControlManagementHttpHandlerTests.java +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/SourceControlManagementHttpHandlerTests.java @@ -36,6 +36,7 @@ import io.cdap.cdap.gateway.handlers.SourceControlManagementHttpHandler; import io.cdap.cdap.internal.app.services.ApplicationLifecycleService; import io.cdap.cdap.internal.app.services.SourceControlManagementService; +import io.cdap.cdap.internal.app.services.SourceControlMetadataRefreshService; import io.cdap.cdap.internal.app.services.http.AppFabricTestBase; import io.cdap.cdap.internal.operation.OperationLifecycleManager; import io.cdap.cdap.metadata.MetadataSubscriberService; @@ -130,12 +131,13 @@ public SourceControlManagementService provideSourceControlManagementService( AuthenticationContext authenticationContext, SourceControlOperationRunner sourceControlRunner, ApplicationLifecycleService applicationLifecycleService, - Store store, OperationLifecycleManager manager, MetricsCollectionService metricsService) { + Store store, OperationLifecycleManager manager, MetricsCollectionService metricsService, + SourceControlMetadataRefreshService sourceControlMetadataRefreshService) { return Mockito.spy(new SourceControlManagementService(cConf, secureStore, transactionRunner, accessEnforcer, authenticationContext, sourceControlRunner, applicationLifecycleService, - store, manager, metricsService)); + store, manager, metricsService, sourceControlMetadataRefreshService)); } }); } diff --git a/cdap-proto/src/main/java/io/cdap/cdap/proto/SourceControlMetadataDetail.java b/cdap-proto/src/main/java/io/cdap/cdap/proto/SourceControlMetadataDetail.java new file mode 100644 index 000000000000..08d09c0ff45b --- /dev/null +++ b/cdap-proto/src/main/java/io/cdap/cdap/proto/SourceControlMetadataDetail.java @@ -0,0 +1,62 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * 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 + * + * http://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. + */ + +package io.cdap.cdap.proto; + +/** + * This class is used for responses of API calls that fetch source control metadata. + */ +public class SourceControlMetadataDetail { + + private final String name; + private final Long lastModified; + private final Boolean syncStatus; + + + /** + * Represents detailed information about source control metadata. + * + * @param name The name of the source control metadata. + * @param lastModified The timestamp when the metadata was last modified. + * @param syncStatus The synchronization status of the metadata with the source control system. + */ + public SourceControlMetadataDetail(String name, Long lastModified, Boolean syncStatus) { + this.name = name; + this.lastModified = lastModified; + this.syncStatus = syncStatus; + } + + public String getName() { + return name; + } + + public Boolean getSyncStatus() { + return syncStatus; + } + + public Long getLastModified() { + return lastModified; + } + + @Override + public String toString() { + return "SourceControlMetadataDetail{" + + "name='" + name + '\'' + + ", lastModified=" + lastModified + + ", syncStatus='" + syncStatus + '\'' + + '}'; + } +}