Skip to content

Implement SCM Pull LRO#15364

Merged
samdgupi merged 1 commit intodevelopfrom
pull-operation
Oct 26, 2023
Merged

Implement SCM Pull LRO#15364
samdgupi merged 1 commit intodevelopfrom
pull-operation

Conversation

@samdgupi
Copy link
Copy Markdown
Contributor

@samdgupi samdgupi commented Oct 16, 2023

Long running operation implementation for SCM pull. It reuses most of the existing git operation logic in InMemorySourceControlOperationRunner which will be refactored when we remove the previous task based implementation.

The actual deploy and mark latest operations are exposed via a new helper interface which will be implemented when the following will be merged
#15336
#15340

@samdgupi samdgupi changed the base branch from develop to operation-api October 16, 2023 18:46
deployed.add(appTobeDeployed.get());
updateMetadata.accept(getOperationMeta());
});
} catch (Exception e) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Catch Exception as we should convert any and all exception to error

@samdgupi samdgupi added build Triggers github actions build 6.10 labels Oct 17, 2023
// If error happened we try to delete all deployed versions
for (ApplicationId appId : deployed) {
try {
helper.deleteAppVersion(appId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we're deleting, we shouldn't do it one by one. That being said, I don't think we need to (or should) delete, as there is no concept of deleting just a single app version today.

Copy link
Copy Markdown
Contributor Author

@samdgupi samdgupi Oct 18, 2023

Choose a reason for hiding this comment

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

We do have appLifecycleService.removeApplicationVersion which I was referring ton but looks like the public API AppLifecycleHttpHandler.deleteAppVersion is deprecated.
The main concern is without deletion this versions left would be surfaced to the user in history creating confusion. Specially in case of a pull of new application as the application would have only one version which is not marked latest.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this isn't the right way to prevent these versions from surfacing in the history for a couple reasons:

  1. It's always possible to see it after the app is added, but before it is marked as latest
  2. The delete can fail

If we are concerned about showing it, the right way to handle it is to add something like an isHidden flag. Trying to delete it here is like a half solution, so I don't think we need to bother doing it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree. Rather than adding a additional flag I think we can have a enum based state field which can be extended later and covers both latest status and hidden status. I have added a followup Jira for this as this is not a blocker for the core feature.

/**
* Request type for {@link PullOperation}.
*/
public class PullOperationRequest {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should name it BulkPullRequest or something like that -- want to differentiate between bulk and single operations

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The plan is we would migrate the single pull to also use the same operation so BulkPull might be misleading.

public static final class AppToPull {

private final String application;
private final String currentGitHash;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is the hash the commit hash or something else? If it's the commit hash, shouldn't that be a top level property and not specific to each app?

Copy link
Copy Markdown
Contributor Author

@samdgupi samdgupi Oct 18, 2023

Choose a reason for hiding this comment

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

As mentioned offline the hash is git-hash of each app config file not the commitid/hash. Hence the property is per app.

*/
public class OperationMeta {
private final Set<OperationResource> resources;
@Nullable
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why is this nullable? Shouldn't it always have a start time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

/**
* Thrown when the application config file is not present in the repository.
*/
public class ApplicationNotFoundException extends NotFoundException {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why do we need this when there is already an ApplicationNotFoundException?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The two excpetions has to different semantical meaning. I have changed the name to GitAppConfigNotFoundException

private final Set<ApplicationId> deployed;

@Inject
PullOperation(InMemorySourceControlOperationRunner scmOpRunner, SourceControlHelper helper) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't this be a SourceControlOperationRunner instead of the InMemory one?

Copy link
Copy Markdown
Contributor Author

@samdgupi samdgupi Oct 18, 2023

Choose a reason for hiding this comment

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

As mentioned in the description for easy reuse of existing code we are putting the new multi push and pull logic in InMemorySourceControlOperationRunner, rather than refactoring out into a new class, which will be done in future.

The PullOperation would always use InMemorySourceControlOperationRunner as the remote runner just creates a task which we do not want.

The SourceControlOperationRunner naming is a bit confusing at this point as the operation here does not refer to LongRunningOperation but git operations.

@samdgupi samdgupi force-pushed the pull-operation branch 2 times, most recently from 509abdf to b281ac8 Compare October 20, 2023 04:52
@samdgupi samdgupi changed the base branch from operation-api to develop October 20, 2023 08:56
if (request.getFilter().getOperationType() != null) {
startFields.add(Fields.stringField(StoreDefinition.OperationRunsStore.TYPE_FIELD,
request.getFilter().getOperationType())
request.getFilter().getOperationType().toString())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@samdgupi to check if we should use .name() instead

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

serialization should use .name(), assuming we are using .valueOf() to deserialize.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This field is not use for serialisation but to index the status separately.

@samdgupi samdgupi requested a review from albertshau October 20, 2023 11:09
@samdgupi samdgupi marked this pull request as ready for review October 20, 2023 11:09
RepositoryConfig repoConfig = getRepositoryMeta(appRef.getParent()).getConfig();
SourceControlMeta latestMeta = store.getAppSourceControlMeta(appRef);
PullAppResponse<?> pullResponse = sourceControlOperationRunner.pull(new PulAppOperationRequest(appRef, repoConfig));
PullAppResponse<?> pullResponse = sourceControlOperationRunner.pull(new PullAppOperationRequest(appRef, repoConfig));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please make sure all checkstyle issues are addressed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

* concrete implementation is because the git operations should always run inMemory.
* @param helper provides utilities to provide app-fabric exposed functionalities.
*/
public PullAppsOperation(@Assisted PullAppsRequest request,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

don't need this annotation

Copy link
Copy Markdown
Contributor Author

@samdgupi samdgupi Oct 23, 2023

Choose a reason for hiding this comment

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

Forgot the Inject annotation. We want the runner and helper to be injected by guice based on where the operation is running but the caller to pass the request only.

The other option is to inject runner/helper in the caller to avoid assisted injection. But that will make the caller be aware of all possible operation inputs that need to be injected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see, that makes more sense. Though the constructor should be package private if we're using injection.

* Provides various helper methods for source control operations. Would be implemented for both
* running in app-fabric and running in workers.
*/
public interface SourceControlHelper {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it should be divided by functionality (ex: separate application and repository interfaces) so that it is more modular and re-usable. We don't really have these types of util or helper classes anywhere in the code because they tend to accumulate random things that don't really have to do with each other.

if (request.getFilter().getOperationType() != null) {
startFields.add(Fields.stringField(StoreDefinition.OperationRunsStore.TYPE_FIELD,
request.getFilter().getOperationType())
request.getFilter().getOperationType().toString())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

serialization should use .name(), assuming we are using .valueOf() to deserialize.

OperationError err = operation.run(mockContext).get();

Assert.assertNull(err);
Mockito.verify(mockHelper, Mockito.times(4)).deployAppVersion(Mockito.any(), Mockito.any());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In general, we try to avoid using Mockito when it is not needed, especially if it is being used to mock our own classes. It is generally more difficult to understand, and it tends to encourage tests that break when the implementation of a class changes instead of testing the actual APIs contracts. For example, if we ever added a way to deploy multiple apps in a single call, this test would break even if the class was functioning properly. Or if we change it not to use this helper, the test would need to be rewritten.

It would be better if the run() method returned something more useful, like the collection of OperationResources it added.

Copy link
Copy Markdown
Contributor Author

@samdgupi samdgupi Oct 24, 2023

Choose a reason for hiding this comment

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

Updated the LongRunningOperation interface and checking the returned resources

String versionId = RunIds.generate().getId();
appTobeDeployed.set(new ApplicationId(context.getRunId().getNamespace(),
response.getApplicationName(), versionId));
helper.deployAppVersion(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

keep in mind that an operation needs to be idempotent in case the system dies and needs to resume the operation on restart. So while there is nothing technically wrong with running through the entire request again, it would be better to look up the operation resources first to see if a good chunk of it is already done.

This can be an improvement added in a future PR, but please be sure to add a jira and TODO for it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Though this also directly contradicts with the fact that it appears like the RepositoryConfig is allowed to change in between calls.

The exact behavior should be clearly defined somewhere, at least in the javadocs. I'm not sure how we want it to behave in all these edge cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Making this idempotent is hard as you mentioned the repsoitory config and/or the git state can change (force push). I will add a TODO as an future investigation improvement.

Also I do not think we have any requirements to add ability to resume for git operations.

The current expected behaviour I will add in the javadocs. We would also need to update the user documentaion/guide.

}

@Override
public ListenableFuture<OperationError> run(LongRunningOperationContext context) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

normally the future type is whatever the return value of the corresponding blocking call would be, with any errors thrown as exceptions. It would make more sense for this to return information about all the applications that were deployed, or something along those lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  1. The reason I am returning OperationError is to support operations with partial failures i.e if the operation wants to continue processing the whole batch and accumulate any error happening for each resources. Throwing exception does not feel like a good model.
  2. We don't have any usage for returning the operated/created resources here. As we want to know exactly what resources were changed in case the runtime crashed, we want to update the resources in operation metadata if and when the resource is changed through the updateResources method in the LongRunningOperationContext

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the interface based on discussion.

Mockito.verify(mockHelper, Mockito.times(1)).markAppVersionsLatest(Mockito.any());
// As runIds are generated inside operation we cannot actually check
// the generated resource uris in metadata
Assert.assertEquals(4, gotResource.size());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this should be comparing the exact contents, not just the collection size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

/**
* Thrown when the application config file is not present in the repository.
*/
public class GitAppConfigNotFoundException extends NotFoundException {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Git -> SourceControl

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done


@Inject
InMemorySourceControlOperationRunner(RepositoryManagerFactory repoManagerFactory) {
public InMemorySourceControlOperationRunner(RepositoryManagerFactory repoManagerFactory) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

injected constructors should almost never be public to prevent manual constructor outside of unit tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was needed to construct InMemorySourceControlOperationRunner in PullOperation test. I will try to find a better way

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@samdgupi samdgupi self-assigned this Oct 23, 2023
@samdgupi samdgupi force-pushed the pull-operation branch 3 times, most recently from 2976dce to 4a575fe Compare October 24, 2023 19:00
@samdgupi samdgupi requested a review from albertshau October 24, 2023 19:04
Copy link
Copy Markdown
Contributor

@albertshau albertshau left a comment

Choose a reason for hiding this comment

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

few minor comments

* concrete implementation is because the git operations should always run inMemory.
* @param helper provides utilities to provide app-fabric exposed functionalities.
*/
public PullAppsOperation(@Assisted PullAppsRequest request,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see, that makes more sense. Though the constructor should be package private if we're using injection.


// TODO(samik, CDAP-20855) Investigate and implement the cleanup for created versions in case of error

// TODO(samik) Update this after along with the runner implementation
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this a todo for the near future (current release) or for a future release? If it will be here beyond the current release, it would be better to make the interface a blocking one instead of pretending it is asynchronous. Then make everything async in some future PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(either way, this can be done in a follow up PR)

new RemoteExceptionProvider<>(NotFoundException.class, NotFoundException::new);
new RemoteExceptionProvider<>(NotFoundException.class, NotFoundException::new);
private final RemoteExceptionProvider<SourceControlAppConfigNotFoundException> gitAppNotFoundExceptionProvider =
new RemoteExceptionProvider<>(SourceControlAppConfigNotFoundException.class, SourceControlAppConfigNotFoundException::new);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

checkstyle

@samdgupi samdgupi force-pushed the pull-operation branch 4 times, most recently from 48e37d2 to c789ca9 Compare October 26, 2023 10:00
@samdgupi samdgupi merged commit de4a284 into develop Oct 26, 2023
@samdgupi samdgupi deleted the pull-operation branch October 26, 2023 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.10 build Triggers github actions build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants