|
25 | 25 | import com.aws.greengrass.deployment.errorcode.DeploymentErrorCode;
|
26 | 26 | import com.aws.greengrass.deployment.errorcode.DeploymentErrorCodeUtils;
|
27 | 27 | import com.aws.greengrass.deployment.exceptions.DeploymentException;
|
| 28 | +import com.aws.greengrass.deployment.exceptions.DeploymentRejectedException; |
28 | 29 | import com.aws.greengrass.deployment.exceptions.InvalidRequestException;
|
29 | 30 | import com.aws.greengrass.deployment.exceptions.MissingRequiredCapabilitiesException;
|
30 | 31 | import com.aws.greengrass.deployment.model.Deployment;
|
@@ -555,13 +556,34 @@ private void createNewDeployment(Deployment deployment) {
|
555 | 556 | if (deploymentTask == null) {
|
556 | 557 | return;
|
557 | 558 | }
|
558 |
| - deploymentStatusKeeper.persistAndPublishDeploymentStatus(deployment.getId(), |
559 |
| - deployment.getGreengrassDeploymentId(), deployment.getConfigurationArn(), |
560 |
| - deployment.getDeploymentType(), JobStatus.IN_PROGRESS.toString(), new HashMap<>(), |
561 |
| - deployment.getDeploymentDocumentObj().getRootPackages()); |
562 | 559 |
|
563 |
| - if (DEFAULT.equals(deployment.getDeploymentStage())) { |
| 560 | + /* |
| 561 | + * Enforce deployments are received for a given deployment target (thing or thingGroup) in sequence such |
| 562 | + * that old deployments for that target does not override a new deployment. |
| 563 | + */ |
| 564 | + if (checkIfDeploymentReceivedIsStale(deployment.getDeploymentDocumentObj(), deployment.getDeploymentType())) { |
| 565 | + logger.atInfo().log("Nucleus has a newer deployment for '{}' target. Rejecting the deployment", |
| 566 | + deployment.getDeploymentDocumentObj().getGroupName()); |
| 567 | + Topics lastDeployment = config.lookupTopics(DeploymentService.GROUP_TO_LAST_DEPLOYMENT_TOPICS, |
| 568 | + deployment.getDeploymentDocumentObj().getGroupName()); |
| 569 | + |
| 570 | + String lastDeploymentConfigArn = |
| 571 | + Coerce.toString(lastDeployment.find(GROUP_TO_LAST_DEPLOYMENT_CONFIG_ARN_KEY)); |
| 572 | + |
| 573 | + updateDeploymentResultAsRejected(deployment, deploymentTask, new DeploymentRejectedException(String.format( |
| 574 | + "Nucleus has a newer deployment for '%s' target deployed by '%s'. Rejecting the " |
| 575 | + + "deployment from '%s'", deployment.getDeploymentDocumentObj().getGroupName(), |
| 576 | + lastDeploymentConfigArn, deployment.getDeploymentDocumentObj().getConfigurationArn()), |
| 577 | + DeploymentErrorCode.REJECTED_STALE_DEPLOYMENT)); |
| 578 | + return; |
| 579 | + } else { |
| 580 | + deploymentStatusKeeper.persistAndPublishDeploymentStatus(deployment.getId(), |
| 581 | + deployment.getGreengrassDeploymentId(), deployment.getConfigurationArn(), |
| 582 | + deployment.getDeploymentType(), JobStatus.IN_PROGRESS.toString(), new HashMap<>(), |
| 583 | + deployment.getDeploymentDocumentObj().getRootPackages()); |
| 584 | + } |
564 | 585 |
|
| 586 | + if (DEFAULT.equals(deployment.getDeploymentStage())) { |
565 | 587 | try {
|
566 | 588 | context.get(KernelAlternatives.class).cleanupLaunchDirectoryLinks();
|
567 | 589 | deploymentDirectoryManager.createNewDeploymentDirectory(deployment.getGreengrassDeploymentId());
|
@@ -615,6 +637,73 @@ private void createNewDeployment(Deployment deployment) {
|
615 | 637 | cancellable);
|
616 | 638 | }
|
617 | 639 |
|
| 640 | + /* |
| 641 | + * Enforce deployments are received for a given deployment target (thing or thingGroup) in sequence such |
| 642 | + * that old deployments for that target does not override a new deployment. |
| 643 | + * |
| 644 | + * For thing deployments, we don't consider them here as they are always in sequence and always for only |
| 645 | + * one target. |
| 646 | + * |
| 647 | + * For thingGroup deployments sent to different targets (thingGroup A & B), nucleus allows components from |
| 648 | + * both groups to be deployment as long as they don't have a conflicting component versions. This |
| 649 | + * behavior is not changed. |
| 650 | + * |
| 651 | + * For thingGroup deployments sent to the same target (thingGroup A) are always in sequence, however if |
| 652 | + * receive a bad/stale deployment due to cloud error we don't want that stale deployment to override a |
| 653 | + * new deployment already performed on device. |
| 654 | + * |
| 655 | + * For a subgroup deployments targeted for a parent fleet group (subgroup A1, A2 & A3 targeted for |
| 656 | + * thingGroup A), as there could be multiple subgroup deployments each of these sent as different jobs to |
| 657 | + * the device could be received in any order yielding an unpredictable behavior. To resolve this, nucleus |
| 658 | + * enforces processing these subgroup deployment in-order of their creation irrespective of when these |
| 659 | + * signals are received. For example: |
| 660 | + * |
| 661 | + * Order of deployment creation is: A1, A2, A3 |
| 662 | + * So these, have to be processed in this order. |
| 663 | + * |
| 664 | + * Order of deployments received: A2, A1, A3 |
| 665 | + * then A2 and A3 deployment will succeed, but A1 would be rejected as nucleus has already processed |
| 666 | + * newer deployment A2. |
| 667 | + * |
| 668 | + * @return true if deployment is considered stale, false otherwise |
| 669 | + */ |
| 670 | + private boolean checkIfDeploymentReceivedIsStale(DeploymentDocument deploymentDocument, |
| 671 | + DeploymentType deploymentType) { |
| 672 | + // Check if group deployment |
| 673 | + boolean isGroupDeployment = Deployment.DeploymentType.IOT_JOBS.equals(deploymentType) |
| 674 | + && deploymentDocument.getGroupName() != null; |
| 675 | + |
| 676 | + // if not a group deployment, then not stale |
| 677 | + if (!isGroupDeployment) { |
| 678 | + return false; |
| 679 | + } |
| 680 | + |
| 681 | + // Get timestamp for the root target group |
| 682 | + Topics lastDeployment = config |
| 683 | + .lookupTopics(DeploymentService.GROUP_TO_LAST_DEPLOYMENT_TOPICS, deploymentDocument.getGroupName()); |
| 684 | + |
| 685 | + long timestamp = Coerce.toLong(lastDeployment.find(GROUP_TO_LAST_DEPLOYMENT_TIMESTAMP_KEY)); |
| 686 | + |
| 687 | + // if don't have last deployment detail, then its a new deployment |
| 688 | + if (timestamp == 0 || deploymentDocument.getTimestamp() == null) { |
| 689 | + return false; |
| 690 | + } |
| 691 | + |
| 692 | + // if the new deployment creation timestamp is smaller than last deployment creation timestamp then its stale |
| 693 | + return deploymentDocument.getTimestamp() < timestamp; |
| 694 | + } |
| 695 | + |
| 696 | + private void updateDeploymentResultAsRejected(Deployment deployment, DeploymentTask deploymentTask, |
| 697 | + Throwable rejectionCause) { |
| 698 | + |
| 699 | + DeploymentResult result = new DeploymentResult(DeploymentResult.DeploymentStatus.REJECTED, rejectionCause); |
| 700 | + |
| 701 | + CompletableFuture<DeploymentResult> process = CompletableFuture.completedFuture(result); |
| 702 | + |
| 703 | + currentDeploymentTaskMetadata = |
| 704 | + new DeploymentTaskMetadata(deployment, deploymentTask, process, new AtomicInteger(1), false); |
| 705 | + } |
| 706 | + |
618 | 707 | private void updateDeploymentResultAsFailed(Deployment deployment, DeploymentTask deploymentTask,
|
619 | 708 | boolean completeExceptionally, Throwable e) {
|
620 | 709 | DeploymentResult result = new DeploymentResult(DeploymentStatus.FAILED_NO_STATE_CHANGE, e);
|
|
0 commit comments