Skip to content

Conversation

kstribrnAmzn
Copy link
Member

Issue #, if available:
#212

Description of changes:
This change updates how Shadow document metadata is calculated. For a simple document updates which remove only top key value pairs, the behavior will not change. For a more complicated document where nested objects are removed, the behavior is updated to keep the empty parent object (which matches IoT Shadow).

Why is this change necessary:
By removing empty objects from the metadata the shadow manager is calculating the ShadowDocMetadata.desired (or reported) field to null which results in a later NullPointerException when building the updated metadata later.

How was this change tested:
Added unit tests as well as performed manual testing against the case mentioned in the issue.

Any additional information or context required to review the change:
It's useful to know how Jackson JsonNode's function

Checklist:

  • [N/A] Updated the README if applicable
  • Updated or added new unit tests
  • [Pending] Updated or added new integration tests
  • [N/A] If your code makes a remote network call, it was tested with a proxy

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@kstribrnAmzn
Copy link
Member Author

Some manual testing logs snippets. In this case I created a shadow document and then set a field to null to remove it. For the case of removing a simple value node (aka JSON value) the shadow document is updated to

GetThingShadowResponse(payload=b'{"state":{},"metadata":{},"version":24,"timestamp":1759530985}') : {'payload': '<payload>'}.

Now for the case of removing a child key-value pair, it is shown that the parent structure maintains.

GetThingShadowResponse(payload=b'{"state":{"desired":{"created":{}},"delta":{"created":{}}},"metadata":{"desired":{"created":{}}},"version":22,"time
stamp":1759530294}') : {'payload': '<payload>'}.

saranyailla
saranyailla previously approved these changes Oct 8, 2025
@saranyailla saranyailla self-requested a review October 8, 2025 04:40
@kstribrnAmzn kstribrnAmzn force-pushed the nullFieldDeletion branch 2 times, most recently from bc594e6 to 4338bc3 Compare October 10, 2025 16:24
Copy link

github-actions bot commented Oct 10, 2025

Unit Tests Coverage Report

File Coverage Lines Branches
All files 83% 88% 78%

Minimum allowed coverage is 65%

Generated by 🐒 cobertura-action against c62b7d2

Copy link

github-actions bot commented Oct 10, 2025

Integration Tests Coverage Report

File Coverage Lines Branches
All files 73% 76% 69%

Minimum allowed coverage is 45%

Generated by 🐒 cobertura-action against c62b7d2

By removing empty objects from the metadata we
are setting the ShadowDocMetadata field to null
which results in a later NullPointerException when
building the updated metadata.

issue - #212
@kstribrnAmzn kstribrnAmzn dismissed saranyailla’s stale review October 10, 2025 21:34

Updates have happened since review.

@kstribrnAmzn kstribrnAmzn marked this pull request as ready for review October 10, 2025 21:35
@kstribrnAmzn
Copy link
Member Author

Some notes from manual testing I performed. There were 4 tests cases I tried - they were executed serially to build off each other. Test cases 2-4 are also covered by unit tests now.

  1. Create a shadow locally with a child object
  2. Remove the child object from the shadow (this is the error in (ShadowManager): NullPointerException on shadow desired state field deletion #212)
  3. Add a new, unique child object to the same parent object
  4. Delete the child object from the previous step while adding another unique child object

I added a few logs to the shadow manager to help me verify this but the logs below should be fairly explanatory. I logged at warn to ease finding the logs:

// Creating the object
2025-10-10T20:27:00.922Z [WARN] (AwsEventLoop 1) com.aws.greengrass.shadowmanager.model.ShadowStateMetadata: [Kody] Final desired state. {Patch Desired={"mystate":{"mysub":{"timestamp":1760128020}}}, Metadata Patch={"desired":{"mystate":{"mysub":{"timestamp":1760128020}}},"reported":{"uuid":{"timestamp":1760128020}}}, Updated Desired={"mystate":{"mysub":{"timestamp":1760128020}}}}
 2025-10-10T20:27:00.922Z [WARN] (AwsEventLoop 1) com.aws.greengrass.shadowmanager.model.ShadowStateMetadata: [Kody] Final reported state. {Patch Reported={"uuid":{"timestamp":1760128020}}, Updated Reported={"uuid":{"timestamp":1760128020}}}
// Read back through dummy component
2025-10-10T20:27:00.980Z [INFO] (Copier) com.example.ShadowOps: stdout. GET LOCAL (CREATED): GetThingShadowResponse(payload=b'{"state":{"desired":{"mystate":{"mysub":"123"}},"reported":{"uuid":"f130d5a4-1dce-4fae-8f52-b395a2ded88a"},"delta":{"mystate":{"mysub":"123"}}},"metadata":{"desired":{"mystate":{"mysub":{"timestamp":1760128020}}},"reported":{"uuid":{"timestamp":1760128020}}},"version":1,"timestamp":1760128020}') : {'payload':...

    
// Remove the child
2025-10-10T20:27:00.990Z [WARN] (AwsEventLoop 1) com.aws.greengrass.shadowmanager.model.ShadowStateMetadata: [Kody] Final desired state. {Patch Desired={"mystate":{"mysub":null}}, Metadata Patch={"desired":{"mystate":{}}}, Updated Desired={"mystate":{}}}
// Read back through dummy component
2025-10-10T20:27:01Z [INFO] (Copier) com.example.ShadowOps: stdout. GET LOCAL (REMOVED): GetThingShadowResponse(payload=b'{"state":{"desired":{"mystate":{}},"reported":{"uuid":"f130d5a4-1dce-4fae-8f52-b395a2ded88a"}},"metadata":{"desired":{"mystate":{}},"reported":{"uuid":{"timestamp":1760128020}}},"version":2,"timestamp":1760128020}') : {'payload':...

// Add new child
2025-10-10T20:27:01.006Z [WARN] (AwsEventLoop 1) com.aws.greengrass.shadowmanager.model.ShadowStateMetadata: [Kody] Final desired state. {Patch Desired={"mystate":{"addedfield":{"timestamp":1760128021}}}, Metadata Patch={"desired":{"mystate":{"addedfield":{"timestamp":1760128021}}}}, Updated Desired={"mystate":{"addedfield":{"timestamp":1760128021}}}}
// Read back through dummy component
2025-10-10T20:27:01.026Z [INFO] (Copier) com.example.ShadowOps: stdout. GET LOCAL (ADDED): GetThingShadowResponse(payload=b'{"state":{"desired":{"mystate":{"addedfield":"123"}},"reported":{"uuid":"f130d5a4-1dce-4fae-8f52-b395a2ded88a"},"delta":{"mystate":{"addedfield":"123"}}},"metadata":{"desired":{"mystate":{"addedfield":{"timestamp":1760128021}}},"reported":{"uuid":{"timestamp":1760128020}}},"version":3,"timestamp":1760128021}') : {'payload':...


// Remove + Add child
2025-10-10T20:27:01.035Z [WARN] (AwsEventLoop 1) com.aws.greengrass.shadowmanager.model.ShadowStateMetadata: [Kody] Final desired state. {Patch Desired={"mystate":{"addedfield":null,"newerfield":{"timestamp":1760128021}}}, Metadata Patch={"desired":{"mystate":{"newerfield":{"timestamp":1760128021}}}}, Updated Desired={"mystate":{"newerfield":{"timestamp":1760128021}}}}
// Read back through dummy component
2025-10-10T20:27:01.046Z [INFO] (Copier) com.example.ShadowOps: stdout. GET LOCAL (EXCHANGED): GetThingShadowResponse(payload=b'{"state":{"desired":{"mystate":{"newerfield":"winner"}},"reported":{"uuid":"f130d5a4-1dce-4fae-8f52-b395a2ded88a"},"delta":{"mystate":{"newerfield":"winner"}}},"metadata":{"desired":{"mystate":{"newerfield":{"timestamp":1760128021}}},"reported":{"uuid":{"timestamp":1760128020}}},"version":4,"timestamp":1760128021}') : {'payload':...

For the desired values I added the following logs here. The reported logs are similar.

        final JsonNode patchDesired = metadataPatchWithRemovedFields.get(SHADOW_DOCUMENT_STATE_DESIRED);
        if (!isNullOrMissing(patchDesired)) {
            desired = nullIfEmpty(merge(state.getDesired(), desired, patchDesired));
            logger.atWarn()
                    .kv("Metadata Patch", metadataPatchWithoutRemovedFields.toString())
                    .kv("Updated Desired", desired.toString())
                    .kv("Patch Desired", patchDesired.toString())
                    .log("[Kody] Final desired state");
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants