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

add the internal marklatest api #15336

Merged
merged 1 commit into from
Oct 24, 2023
Merged

Conversation

GnsP
Copy link
Contributor

@GnsP GnsP commented Sep 27, 2023

This PR adds:

  1. Support for a query param skipMakingLatest in the PUT api/v3internal/namespaces/:namespace/apps/:appId . When this param is true, the application is deployed, but it's not marked as latest.
  2. Adds an internal api api/v3internal/namespaces/:namespace/apps/markLatest. This api takes a list of appIds (with versionIds) and marks the specified versions of the apps as latest.

JIRA: CDAP-20857

@GnsP GnsP added the build Triggers github actions build label Oct 3, 2023
@GnsP GnsP force-pushed the scm-mark-latest-api branch from d05f134 to 3d743cd Compare October 5, 2023 14:41
Copy link
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.

the unit tests should be focused on the DefaultStore implementation. There should be a bunch of test cases added in DefaultStoreTest ensuring nothing breaks with this new paradigm where 'isLatest' no longer means the most recently added version. For example, I'm not sure if there is any logic somewhere that is making that assumption that now break.

* Marks an existing application as latest
*
* @param id application id
* @throws Exception if the app cannot be marked latest
Copy link
Contributor

Choose a reason for hiding this comment

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

avoid throws Exception, it makes it impossible for the caller to do properly error handling (there are existing examples of this in the code already, but they are bad examples). Any exception that is thrown should be more specific.

Should also clearly define the expected behavior in certain edge cases. For example, what should happen if one of the provided application ids doesn't exist? What if multiple versions of the same application are given?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added more specific exceptions.
The multiple versions exception is handled in the service layer.

Copy link
Contributor

Choose a reason for hiding this comment

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

javadoc throws needs to be updated

Copy link
Contributor

Choose a reason for hiding this comment

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

also, it should define behavior in some of the edge cases like I mentioned above. For example, what happens if a given application does not exist? Does it get ignored? Does it throw some sort of Exception?

From a SCM batch pull perspective, this would happen if in the middle of the operation we deployed a new non-latest version of an app, and then somebody goes and deletes the app before we get to this mark latest part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now it throws ApplicationNotFoundException in this case.

}
}

private BodyConsumer deployAppFromArtifact(final ApplicationId appId, final boolean skipMakingLatest)
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks like duplicate code, can it be put in the abstract class? or are there logical differences between them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had considered to put it in the abstract class. But there was a minor difference between the internal and public implementations (in case of access validations). Will try and refactor it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just adding a TODO with a JIRA task for now. Will take it up in the AppLifecycleService cleanup PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

The question was never answered, is this duplicate code or is it different some how? If it is different, please highlight what is changed and why. If it's the same, we should just move it now and not have a todo.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved to the base class and updated usage.

* Marks an existing application as latest
*
* @param id application id
* @throws Exception if the app cannot be marked latest
Copy link
Contributor

Choose a reason for hiding this comment

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

javadoc throws needs to be updated

* Marks an existing application as latest
*
* @param id application id
* @throws Exception if the app cannot be marked latest
Copy link
Contributor

Choose a reason for hiding this comment

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

also, it should define behavior in some of the edge cases like I mentioned above. For example, what happens if a given application does not exist? Does it get ignored? Does it throw some sort of Exception?

From a SCM batch pull perspective, this would happen if in the middle of the operation we deployed a new non-latest version of an app, and then somebody goes and deletes the app before we get to this mark latest part.

@GnsP GnsP force-pushed the scm-mark-latest-api branch 2 times, most recently from 899e792 to 93585a8 Compare October 16, 2023 12:31
Copy link
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.

there are still no tests for the actual logic that was added around changing the state in the Store.

There are also a lot of repeat comments. Please make sure to address all comments before the next review.

}
}

private BodyConsumer deployAppFromArtifact(final ApplicationId appId, final boolean skipMakingLatest)
Copy link
Contributor

Choose a reason for hiding this comment

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

The question was never answered, is this duplicate code or is it different some how? If it is different, please highlight what is changed and why. If it's the same, we should just move it now and not have a todo.

@GnsP GnsP force-pushed the scm-mark-latest-api branch from c5f7936 to 96321a9 Compare October 18, 2023 05:46
@@ -200,4 +219,72 @@ public void getAppDetailForVersion(HttpRequest request, HttpResponder responder,
: applicationLifecycleService.getAppDetail(appId);
responder.sendJson(HttpResponseStatus.OK, GSON.toJson(appDetail));
}

/**
* Deploy an application.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please mention why we have to deploy methods one inetrnally one externally

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the javadoc comment and mentioned why this internal API is needed.

StructuredTable appSpecTable = getApplicationSpecificationTable();

// First unmark the current latest version
ApplicationMeta latest = getLatest(id.getAppReference());
Copy link
Contributor

Choose a reason for hiding this comment

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

getLatest reads the entire row and deserializes into ApplicationMeta, which is more expensive than needed. It also has a bunch of extra reads to handle rows that were in the table before the isLatest column was added.

For this method, none of that extra logic is needed. We just need to use the StructuredTable.update() method to first set whatever has isLatest == true to false, and then to set isLatest to true for the id given.

Copy link
Contributor

Choose a reason for hiding this comment

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

for the existence check, I don't remember if trying to update a row that doesn't exist ends up throwing an exception or if it becomes a no-op. If it's a no-op and we still need to do the read, we should be using StructuredTable.read() on just the relevant fields without deserializing anything into an ApplicationMeta.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the update method does not throw an exception when the row does not exist. It's a no-op in that case.

Updated the code to read the application row to check if it exists already.

@GnsP GnsP force-pushed the scm-mark-latest-api branch from 96321a9 to 27c5ca1 Compare October 19, 2023 06:07
@QueryParam("skipMakingLatest") final boolean skipMakingLatest)
throws BadRequestException, NamespaceNotFoundException, AccessException {
String versionId = ApplicationId.DEFAULT_VERSION;
if (Feature.LIFECYCLE_MANAGEMENT_EDIT.isEnabled(featureFlagsProvider)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a new feature flag for promotion to prod and only enable this api if that featurev flag is switched on

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added feature flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed in this comment, the feature flag check is not needed here.

@GnsP GnsP force-pushed the scm-mark-latest-api branch 9 times, most recently from 28b529d to c0a2efd Compare October 19, 2023 19:09
@PathParam("namespace-id") final String namespaceId,
@PathParam("app-id") final String appId,
@QueryParam("skipMarkingLatest") final boolean skipMarkingLatest) throws Exception {
if (!Feature.PIPELINE_PROMOTION_TO_PRODUCTION.isEnabled(featureFlagsProvider)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need these flags to protect internal APIs, these are normally only done for user facing APIs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

@Path("/apps/markLatest")
public void markApplicationsAsLatest(FullHttpRequest request, HttpResponder responder,
@PathParam("namespace-id") final String namespace) throws Exception {
if (!Feature.PIPELINE_PROMOTION_TO_PRODUCTION.isEnabled(featureFlagsProvider)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

similar comment, don't need to do this for an internal API

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

// First find and unmark the current latest version
Range latestRange = getLatestApplicationRange(id.getAppReference());
try (CloseableIterator<StructuredRow> iterator =
appSpecTable.scan(latestRange, 1, StoreDefinition.AppMetadataStore.CREATION_TIME_FIELD,
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't need to scan with a range here, this should just be a read() call on getLatestApplicationKeys().

There is a scan happening elsewhere in the code to handle old rows that were from before the isLatest column existed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The read method expects a list of fields where the keys are prefix of the primary keys (in this case, the the list of keys for the read operation should be a prefix of the primary keys [namespace, application, version]).

The getLatestApplicationKeys() method returns the list of fields [namespace, application, latest], which is not a prefix of the primary keys. So, the read method throws an error io.cdap.cdap.spi.data.InvalidFieldException.

As we do not know the version of the current latest beforehand, we need to scan the table to find the current latest version with the key range [namespace, application, latest]. Then only we can update the corresponding row to set the latest column to false.

Copy link
Contributor

Choose a reason for hiding this comment

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

hm it should really allow primary key fields and index fields in there.

In that case, we should be using the scan(range, limit, filter) method, as we don't need to order by anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to use the scan(range, limit, filterIndexes) method.


// check if the application being marked latest is already present in the table
// if not, then an ApplicationNotFoundException should be thrown
List<Field<?>> fields = getApplicationPrimaryKeys(id);
Copy link
Contributor

Choose a reason for hiding this comment

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

this existence check should happen first to avoid any unnecessary work if it doesn't exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved this check to the top, so that it can error out early (before the rather expensive scan operation is performed).

Assert.assertEquals(appName, appDetail.getName());
Assert.assertEquals(ApplicationId.DEFAULT_VERSION, appDetail.getAppVersion());

setPromotionToProdFlag(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

don't do stateful things like this in unit tests. If this test fails above, it won't reset anything and will break all the other tests in this class.

Instead, set the "normal" state in a @Before method to ensure it is always set for each test. If a test requires a flag to be set to a different value, do it at the start of the test.

Copy link
Contributor

Choose a reason for hiding this comment

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

that being said, I don't think there is any need to set this in the test, just make it default to enabled. We're not impacting any of the existing logic with these new APIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

I have removed the promotionToProd flag as it's not necessary for internal APIs.

ApplicationId appId = new ApplicationId("account1", "app1");
ApplicationMeta appMeta = new ApplicationMeta("app1", Specifications.from(new FooApp()),
new ChangeDetail(null, null, null,
System.currentTimeMillis()), null, false);
Copy link
Contributor

Choose a reason for hiding this comment

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

don't use values like current time in unit tests, as you can never compare them to expected values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the tests to store the creation time as a variable (instead of passing the value directly) so that we can compare it with the actual value when needed.

store.addApplication(appId, appMeta);

ApplicationMeta storedMeta = store.getApplicationMetadata(appId);
Assert.assertNotNull(storedMeta);
Copy link
Contributor

Choose a reason for hiding this comment

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

assertNotNull is not good enough, it should be checking the actual data.

Copy link
Contributor

Choose a reason for hiding this comment

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

similar comment for all the other tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed the null checks and added checks to verify the actual metadata.

@@ -41,7 +41,8 @@ public enum Feature {
WRANGLER_PRECONDITION_SQL("6.9.1"),
WRANGLER_EXECUTION_SQL("6.10.0"),
WRANGLER_SCHEMA_MANAGEMENT("6.10.0"),
NAMESPACED_SERVICE_ACCOUNTS("6.10.0");
NAMESPACED_SERVICE_ACCOUNTS("6.10.0"),
PIPELINE_PROMOTION_TO_PRODUCTION("6.10.0");
Copy link
Contributor

Choose a reason for hiding this comment

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

should not name the feature something vague like this, it should be specific to the actual functionality being controlled. In this case, it is controlling whether bulk source control operations are enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the flag from this PR. As mentioned in this comment we do not need feature guards for internal APIs.

* @param responder {@link HttpResponder}
* @param namespaceId of the namespace where the app is to be deployed
* @param appId of the app
* @param skipMarkingLatest if true, the app will be deployed but not marked latest
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Please mention what marking latest means. i.e the latest marked version is the one that will be run

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

for (ApplicationId appId : appIds) {
mds.updateLatestApplicationVersion(appId);
}
}, IOException.class, ApplicationNotFoundException.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

@GnsP I thought we want to skip missing versions and continue marking rest of them latest ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was for the updateGitMeta. In case of marking latest, if any of the apps are not marked latest then the entire operation should fail. Because there may be dependencies between pipelines/apps.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense thanks

@@ -109,6 +119,16 @@ public static void stop() {
AppFabricTestHelper.shutdown();
}

private void setPromotionToProdFlag(boolean promotionToProdFlag) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this as we are removing the flag check in the api calls

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has been removed in the latest commit. Need to push the changes after the build is verified locally.

@GnsP GnsP force-pushed the scm-mark-latest-api branch 3 times, most recently from 76f04d4 to 82e5429 Compare October 20, 2023 11:36
for (ApplicationId appId : appIds) {
mds.updateLatestApplicationVersion(appId);
}
}, IOException.class, ApplicationNotFoundException.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense thanks


@Test
public void testMarkAppsAsLatest() throws Exception {
setLcmEditFlag(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we setting lcm as false here ? without lcm marklatest has no usecase

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the test and removed the line setting lcmEditFlag to false.

HttpResponse resp = deployWithoutMarkingLatest(
appId, new AppRequest<>(ArtifactSummary.from(artifactId.toArtifactId())));
Assert.assertEquals(200, resp.getResponseCode());

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please put a comment here why ApplicationNotFoundException is expected here. Usually happy path tests should not have exception check so having a comment would be useful for future contributors.

Also this tests a new app I believe? We also need to check adding versions for a existing app

Copy link
Contributor

Choose a reason for hiding this comment

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

We should also verify the version was actually created with latest field as false

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed offline, I have added comments describing the intended behaviour of the test.

@Test
public void testAddApplicationWithoutMarkingLatest()
throws ConflictException {
long creationTime = System.currentTimeMillis();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I dont think you dont need to create this everytime. This can be a class level final value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. But (for now) I would rather keep these values isolated (contained within the context of each test) without adding class level variables.

new AppVersion(appName, ApplicationId.DEFAULT_VERSION))
));
}

Copy link
Contributor

Choose a reason for hiding this comment

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

We should also have a partial failure test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do have a partial failure testcase. Now I have added javadoc comments on each of the test methods, so that it's easier to check the intended behaviour (verification) of each test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also added the testcase that we discussed in the meeting (with 2 versions of the same application deployed one after another, then the later/second version marked as latest).

Copy link
Contributor

@samdgupi samdgupi left a comment

Choose a reason for hiding this comment

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

LGTM as long as the comments are followed up. Please resolve the build issue

@GnsP GnsP force-pushed the scm-mark-latest-api branch from 82e5429 to 851cdf9 Compare October 20, 2023 14:52
Copy link
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.

just one comment about the scan, otherwise lgtm. Please squash commits before merging.

@GnsP GnsP force-pushed the scm-mark-latest-api branch from 851cdf9 to e783c61 Compare October 21, 2023 15:36
@GnsP GnsP merged commit cd8f7a1 into cdapio:develop Oct 24, 2023
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
build Triggers github actions build
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants