I recently had the opportunity to deep dive on Azure Cosmos DB’s Change Feed feature for a project. In this post I’d like to share a few things I learned and provide clarity around change feed implementation details that were unclear to me from the docs.
About the Azure Cosmos DB Change Feed
If you aren’t familiar with the change feed feature, I would suggest starting with Microsoft’s docs. From the summary description:
Change feed in Azure Cosmos DB is a persistent record of changes to a container in the order they occur. Change feed support in Azure Cosmos DB works by listening to an Azure Cosmos DB container for any changes. It then outputs the sorted list of documents that were changed in the order in which they were modified.Microsoft Docs
This is helpful for a few scenarios. For example, if you need to trigger a process every time Cosmos entities are added or updated. Or you want to process all entities in the container for a batch job.
Note: In this post I’m mainly referring to reading changes for an entire container using the change feed processor method.
The Change Feed Processor Delegate
One of the key items to implementing the change feed processor is to implement a delegate method that ‘handles’ each batch of changes that the processor reads in. The signature looks like this:
static async Task HandleChangesAsync( ChangeFeedProcessorContext context, IReadOnlyCollection<ToDoItem> changes, CancellationToken cancellationToken)
Notice that the collection of changes is really your entity model object (instead of a class provided by Cosmos). I had a few questions about how this part worked in practice, so I’m listing some of the questions I had and what I observed during testing.
Q: Are all the properties on the entity model populated? Or only the fields that changed?
A: When the delegate is called, the model change objects received are fully populated with all of the properties found on the Cosmos entity as it lives in the database. Since all of the fields are present, this means there is no other information available to determine which specific properties on the entity had changed. You would have to keep track of this using some other mechanism.
Q: Does the entity model reflect the current state? Or a point in time?
A: In the delegate handler, the entity change model state reflects the item exactly as it is present in the database right now. It does not reflect a previous point in time (for example if there were multiple changes over a period of time).
Q: How does it handle multiple distinct changes to the same entity?
A: Because the entity model received is the current state of the item, we only get one ‘change’ object per entity. For example– if I change a property in two separate database update calls, only the final update (current state) would be reflected in the change list. The intermediate update would not be received.
Note: this is part of the reason why change feed is really useful for batch processing an entire container of entities. All of the items should only show up in the feed once.
Q: If an item was changed, but then later deleted, what happens?
A: You won’t receive a ‘change’ update in the changes delegate. The only way to capture this scenario is to use a soft-delete procedure with your entities (ex: add an ‘IsDeleted’ bool value, then set that flag instead of deleting the item).
Q: What happens when your change feed processor finishes reading all the items in the container?
A: Working in the batch processing scenario (reading all container objects from the beginning of time until now) — eventually all items will be processed and the HandleChangesAsync() won’t be called again until there are new or modified items in your monitored container.
The Lease Container
The change feed processor uses a Cosmos container to centrally coordinate the work for change feed consumers. This is where change feed processing state is stored. Here are the findings I had for working with lease containers:
Q: Who creates the lease container? Do I have to do this manually?
A: You must create the lease container ahead of time. The client-side / Cosmos SDK doesn’t create this for you.
Q: What is the partition key path for the lease container?
A: When creating any Cosmos container, you must specify a partition key path. The change feed docs don’t actually tell you what partition key path you should use for the lease container, so I used ‘/id’ and that worked fine. The resulting lease container objects do actually have an /id field, so I would bet this is what they expect it to be configured as.
Q: What happens when you delete the contents inside the lease container?
A: This is something I tried just to see how the change feed processor code reacted. If you delete everything inside the lease container (but leave the empty container) — the change feed processor gracefully starts over from the beginning. This could be helpful if you wanted to re-run your batch processing all over again.
Lastly, I want to cover a couple performance items. This is clearly documented– but I want to call out that the change feed processor will consume request units (RUs) on both your monitored container (change feed source), and the lease container. There isn’t a magic side-channel for consuming the change feed.
You can horizontally scale your change feed consumers — but this comes at the cost of increased RUs against your containers. Ensure that you have sufficient RU limits configured to handle the load you plan to use (for change feed processing, and any existing, unrelated traffic running simultaneously). Also have a plan in place for when/if you hit Cosmos rate limiting.