---
title: Charmhub | Deploy Rolling Ops Library and Example Charm using Charmhub - The
  Open Operator Collection
description: Deploy the latest version of Rolling Ops Library and Example Charm on
  any cloud.
url: https://charmhub.io/rolling-ops/libraries/rollingops
---

# Rolling Ops Library and Example Charm

[Canonical](https://charmhub.io/publisher/data-platform "View all packages from Canonical")

* [Canonical](https://charmhub.io/publisher/data-platform "View all packages from Canonical")

Platform:

22.04

stable 5

```
juju deploy rolling-ops
```

[Learn to deploy on juju >](https://juju.is/docs/juju/manage-applications)

[Toggle side navigation](https://charmhub.io/rolling-ops/libraries/rollingops#drawer)

## charms.rolling\_ops.v1.rollingops

* [*Docstrings*Docstrings](https://charmhub.io/rolling-ops/libraries/rollingops)
  [*Code*Source code](https://charmhub.io/rolling-ops/libraries/rollingops/source-code)
* + Download

    Fetch library

    ```
    charmcraft fetch-lib charms.rolling_ops.v1.rollingops
    ```

    [Download rollingops.py](https://charmhub.io/rolling-ops/libraries/rollingops/download)
  + *Last updated* 19 May 2026
  + *Revision* Library version 1.1

Rolling Ops v1 — coordinated rolling operations for Juju charms.

This library provides a reusable mechanism for coordinating rolling operations
across units of a Juju application using a peer-relation distributed lock.

The library guarantees that at most one unit executes a rolling operation at any
time, while allowing multiple units to enqueue operations and participate
in a coordinated rollout.

##### Data model (peer relation)

###### Unit databag

Each unit maintains a FIFO queue of operations it wishes to execute.

Keys:

* `operations`: JSON-encoded list of queued `Operation` objects
* `state`: `"idle"` | `"request"` | `"retry-release"` | `"retry-hold"`
* `executed_at`: UTC timestamp string indicating when the current operation last ran

Each `Operation` contains:

* `callback_id`: identifier of the callback to execute
* `kwargs`: JSON-serializable arguments for the callback
* `requested_at`: UTC timestamp when the operation was enqueued
* `max_retry (optional)`: maximum retry count. `None` means unlimited
* `attempt`: current attempt number

###### Application databag

The application databag represents the global lock state.

Keys:

* `granted_unit`: unit identifier (unit name), or empty
* `granted_at`: UTC timestamp indicating when the lock was granted

##### Operation semantics

* Units enqueue operations instead of overwriting a single pending request.
* Duplicate operations (same `callback_id` and `kwargs`) are ignored if they are
  already the last queued operation.
* When granted the lock, a unit executes exactly one operation (the queue head).
* After execution, the lock is released so that other units may proceed.

##### Retry semantics

* If a callback returns `OperationResult.RETRY_RELEASE` the unit will release the
  lock and retry the operation later.
* If a callback returns `OperationResult.RETRY_HOLD` the unit will keep the
  lock and retry immediately.
* Retry state (`attempt`) is tracked per operation.
* When `max_retry` is exceeded, the failing operation is dropped and the unit
  proceeds to the next queued operation, if any.

##### Scheduling semantics

* Only the leader schedules lock grants.
* If a valid lock grant exists, no new unit is scheduled.
* Requests are preferred over retries.
* Among requests, the operation with the oldest `requested_at` timestamp is selected.
* Among retries, the operation with the oldest `executed_at` timestamp is selected.
* Stale grants (e.g., pointing to departed units) are automatically released.

All timestamps are stored in UTC using ISO 8601 format.

##### Using the library in a charm

###### 1. Declare a peer relation

```
peers:
  restart:
    interface: rolling_op
```

Import this library into src/charm.py, and initialize a RollingOpsManagerV1 in the Charm's
`__init__`. The Charm should also define a callback routine, which will be executed when
a unit holds the distributed lock:

src/charm.py

```
from charms.rolling_ops.v1.rollingops import RollingOpsManagerV1, OperationResult

class SomeCharm(CharmBase):
    def __init__(self, *args):
        super().__init__(*args)

        self.rolling_ops = RollingOpsManagerV1(
            charm=self,
            relation_name="restart",
            callback_targets={
                "restart": self._restart,
                "failed_restart": self._failed_restart,
                "defer_restart": self._defer_restart,
            },
        )

    def _restart(self, force: bool) -> OperationResult:
        # perform restart logic
        return OperationResult.RELEASE

    def _failed_restart(self) -> OperationResult:
        # perform restart logic
        return OperationResult.RETRY_RELEASE

    def _defer_restart(self) -> OperationResult:
        if not self.some_condition():
            return OperationResult.RETRY_HOLD
        # do restart logic
        return OperationResult.RELEASE
```

Request a rolling operation

```
    def _on_restart_action(self, event) -> None:
        self.rolling_ops.request_async_lock(
            callback_id="restart",
            kwargs={"force": True},
            max_retry=3,
    )
```

All participating units must enqueue the operation in order to be included
in the rolling execution.

Units that do not enqueue the operation will be skipped, allowing operators
to recover from partial failures by reissuing requests selectively.

Do not include sensitive information in the kwargs of the callback.
These values will be stored in the databag.

Make sure that callback\_targets is not dynamic and that the mapping
contains the expected values at the moment of the callback execution.

---

Index

* [class RollingOpsNoRelationError](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsnorelationerror)
* [class RollingOpsDecodingError](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsdecodingerror)
* [class RollingOpsInvalidLockRequestError](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsinvalidlockrequesterror)
* [class Operation](https://charmhub.io/rolling-ops/libraries/rollingops#operation)
* + [def \_\_post\_init\_\_(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operation-__post_init__)
* + [def create(
    cls,
    callback\_id,
    kwargs,
    max\_retry)](https://charmhub.io/rolling-ops/libraries/rollingops#operation-create)
* + [def to\_string(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operation-to_string)
* + [def increase\_attempt(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operation-increase_attempt)
* + [def is\_max\_retry\_reached(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operation-is_max_retry_reached)
* + [def from\_string(
    cls,
    data)](https://charmhub.io/rolling-ops/libraries/rollingops#operation-from_string)
* + [def \_\_eq\_\_(
    self,
    other)](https://charmhub.io/rolling-ops/libraries/rollingops#operation-__eq__)
* + [def \_\_hash\_\_(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operation-__hash__)
* [class OperationQueue](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue)
* + [def \_\_init\_\_(
    self,
    operations)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-__init__)
* + [def \_\_len\_\_(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-__len__)
* + [def empty(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-empty)
* + [def peek(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-peek)
* + [def dequeue(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-dequeue)
* + [def increase\_attempt(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-increase_attempt)
* + [def enqueue\_lock\_request(
    self,
    callback\_id,
    kwargs,
    max\_retry)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-enqueue_lock_request)
* + [def to\_string(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-to_string)
* + [def from\_string(
    cls,
    data)](https://charmhub.io/rolling-ops/libraries/rollingops#operationqueue-from_string)
* [class LockIntent](https://charmhub.io/rolling-ops/libraries/rollingops#lockintent)
* [class OperationResult](https://charmhub.io/rolling-ops/libraries/rollingops#operationresult)
* [class Lock](https://charmhub.io/rolling-ops/libraries/rollingops#lock)
* + [def \_\_init\_\_(
    self,
    model,
    relation\_name,
    unit)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-__init__)
* + [def request(
    self,
    callback\_id,
    kwargs,
    max\_retry)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-request)
* + [def retry\_release(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-retry_release)
* + [def retry\_hold(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-retry_hold)
* + [def complete(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-complete)
* + [def release(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-release)
* + [def grant(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-grant)
* + [def is\_granted(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-is_granted)
* + [def should\_run(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-should_run)
* + [def should\_release(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-should_release)
* + [def is\_waiting(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-is_waiting)
* + [def is\_completed(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-is_completed)
* + [def is\_retry(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-is_retry)
* + [def is\_waiting\_retry(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-is_waiting_retry)
* + [def is\_retry\_hold(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-is_retry_hold)
* + [def get\_current\_operation(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-get_current_operation)
* + [def get\_last\_completed(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-get_last_completed)
* + [def get\_requested\_at(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lock-get_requested_at)
* [class LockIterator](https://charmhub.io/rolling-ops/libraries/rollingops#lockiterator)
* + [def \_\_init\_\_(
    self,
    model,
    relation\_name)](https://charmhub.io/rolling-ops/libraries/rollingops#lockiterator-__init__)
* + [def \_\_iter\_\_(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#lockiterator-__iter__)
* [def pick\_oldest\_completed(
  locks
  )](https://charmhub.io/rolling-ops/libraries/rollingops#pick_oldest_completed)
* [def pick\_oldest\_request(
  locks
  )](https://charmhub.io/rolling-ops/libraries/rollingops#pick_oldest_request)
* [class RollingOpsLockGrantedEvent](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopslockgrantedevent)
* [class RollingOpsManagerV1](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsmanagerv1)
* + [def \_\_init\_\_(
    self,
    charm,
    relation\_name,
    callback\_targets)](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsmanagerv1-__init__)
* + [def request\_async\_lock(
    self,
    callback\_id,
    kwargs,
    max\_retry)](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsmanagerv1-request_async_lock)
* [class RollingOpsAsyncWorker](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsasyncworker)
* + [def \_\_init\_\_(
    self,
    charm,
    relation\_name)](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsasyncworker-__init__)
* + [def start(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsasyncworker-start)
* + [def stop(
    self)](https://charmhub.io/rolling-ops/libraries/rollingops#rollingopsasyncworker-stop)
* [def main(
  )](https://charmhub.io/rolling-ops/libraries/rollingops#main)

#### class RollingOpsNoRelationError

Description

Raised if we are trying to process a lock, but do not appear to have a relation yet. None

#### class RollingOpsDecodingError

Description

Raised if the content of the databag cannot be processed. None

#### class RollingOpsInvalidLockRequestError

Description

Raised if the lock request is invalid. None

#### class Operation

Description

A single queued operation. None

Methods

Operation.
\_\_post\_init\_\_(

*self*
)

Description

Validate the class attributes. None

Operation.
create(

cls

,
callback\_id: str

,
kwargs

,
max\_retry
)

Description

Create a new operation from a callback id and kwargs. None

Operation.
to\_string(

*self*
)

Description

Serialize to a string suitable for a Juju databag. None

Operation.
increase\_attempt(

*self*
)

Description

Increment the attempt counter. None

Operation.
is\_max\_retry\_reached(

*self*
)

Description

Return True if attempt exceeds max\_retry (unless max\_retry is None). None

Operation.
from\_string(

cls

,
data: str
)

Deserialize from a Juju databag string.

Operation.
\_\_eq\_\_(

*self*

,
other: object
)

Description

Equal for the operation. None

Operation.
\_\_hash\_\_(

*self*
)

Description

Hash for the operation. None

#### class OperationQueue

Description

In-memory FIFO queue of Operations with encode/decode helpers for storing in a databag. None

Methods

OperationQueue.
\_\_init\_\_(

*self*

,
operations
)

OperationQueue.
\_\_len\_\_(

*self*
)

Description

Return the number of operations in the queue. None

OperationQueue.
empty(

*self*
)

Description

Return True if there are no queued operations. None

OperationQueue.
peek(

*self*
)

Description

Return the first operation in the queue if it exists. None

OperationQueue.
dequeue(

*self*
)

Description

Drop the first operation in the queue if it exists and return it. None

OperationQueue.
increase\_attempt(

*self*
)

Description

Increment the attempt counter for the head operation and persist it. None

OperationQueue.
enqueue\_lock\_request(

*self*

,
callback\_id: str

,
kwargs

,
max\_retry
)

Description

Append operation only if it is not equal to the last enqueued operation. None

OperationQueue.
to\_string(

*self*
)

Description

Encode entire queue to a single string. None

OperationQueue.
from\_string(

cls

,
data: str
)

Decode queue from a string.

#### class LockIntent

Description

Unit-level lock intents stored in unit databags. None

#### class OperationResult

Description

Callback return values. None

#### class Lock

State machine view over peer relation databags for a single unit.

Description

This class is the only component that should directly read/write the peer relation
databags for lock state, queue state, and grant state.

Important:

* All relation databag values are strings.
* This class updates both unit databags and app databags, which triggers
  relation-changed events.

Methods

Lock.
\_\_init\_\_(

*self*

,
model: Model

,
relation\_name: str

,
unit: Unit
)

Lock.
request(

*self*

,
callback\_id: str

,
kwargs

,
max\_retry
)

Enqueue an operation and mark this unit as requesting the lock.

Arguments

callback\_id

identifies which callback to execute.

kwargs

dict of callback kwargs.

max\_retry

None -> unlimited retries, else explicit integer.

Lock.
retry\_release(

*self*
)

Description

Indicate that the operation should be retried but the lock should be released. None

Lock.
retry\_hold(

*self*
)

Description

Indicate that the operation should be retried but the lock should be kept. None

Lock.
complete(

*self*
)

Mark the head operation as completed successfully, pop it from the queue.

Description

Update unit state depending on whether more operations remain.

Lock.
release(

*self*
)

Description

Clear the application-level grant. None

Lock.
grant(

*self*
)

Description

Grant a lock to a unit. None

Lock.
is\_granted(

*self*
)

Description

Return True if the unit holds the lock. None

Lock.
should\_run(

*self*
)

Description

Return True if the lock has been granted to the unit and it is time to execute callback. None

Lock.
should\_release(

*self*
)

Description

Return True if the unit finished executing the callback and should be released. None

Lock.
is\_waiting(

*self*
)

Description

Return True if this unit is waiting for a lock to be granted. None

Lock.
is\_completed(

*self*
)

Description

Return True if this unit is completed callback but still has the grant (leader should clear). None

Lock.
is\_retry(

*self*
)

Description

Return True if this unit requested retry but still has the grant (leader should clear). None

Lock.
is\_waiting\_retry(

*self*
)

Description

Return True if the unit requested retry and is waiting for lock to be granted. None

Lock.
is\_retry\_hold(

*self*
)

Description

Return True if the unit requested retry and wants to keep the lock. None

Lock.
get\_current\_operation(

*self*
)

Description

Return the head operation for this unit, if any. None

Lock.
get\_last\_completed(

*self*
)

Description

Get the time the unit requested a retry of the head operation. None

Lock.
get\_requested\_at(

*self*
)

Description

Get the time the head operation was requested at. None

#### class LockIterator

Description

Iterator over Lock objects for each unit present on the peer relation. None

Methods

LockIterator.
\_\_init\_\_(

*self*

,
model: Model

,
relation\_name: str
)

LockIterator.
\_\_iter\_\_(

*self*
)

Description

Yields a lock for each unit we can find on the relation. None

#### def pick\_oldest\_completed(locks)

Description

Choose the retry lock with the oldest executed\_at timestamp. None

#### def pick\_oldest\_request(locks)

Description

Choose the lock with the oldest head operation. None

#### class RollingOpsLockGrantedEvent

Description

Custom event emitted when the background worker grants the lock. None

#### class RollingOpsManagerV1

Description

Emitters and handlers for rolling ops. None

Methods

RollingOpsManagerV1.
\_\_init\_\_(

*self*

,
charm: CharmBase

,
relation\_name: str

,
callback\_targets
)

Register our custom events.

Description

params:
charm: the charm we are attaching this to.
relation\_name: the peer relation name from metadata.yaml.
callback\_targets: mapping from callback\_id -> callable.

RollingOpsManagerV1.
request\_async\_lock(

*self*

,
callback\_id: str

,
kwargs

,
max\_retry
)

Enqueue a rolling operation and request the distributed lock.

Arguments

callback\_id

Identifier for the callback to execute when this unit is granted
the lock. Must be a non-empty string and must exist in the manager's
callback registry.

kwargs

Keyword arguments to pass to the callback when executed. If omitted,
an empty dict is used. Must be JSON-serializable because it is stored
in Juju relation databags.

max\_retry

Retry limit for this operation. None means unlimited retries.
0 means no retries (drop immediately on first failure). Must be >= 0
when provided.

Description

This method appends an operation (identified by callback\_id and kwargs) to the
calling unit's FIFO queue stored in the peer relation databag and marks the unit as
requesting the lock. It does not execute the operation directly.

#### class RollingOpsAsyncWorker

Description

Spawns and manages the external rolling-ops worker process. None

Methods

RollingOpsAsyncWorker.
\_\_init\_\_(

*self*

,
charm: CharmBase

,
relation\_name: str
)

RollingOpsAsyncWorker.
start(

*self*
)

Description

Start a new worker process. None

RollingOpsAsyncWorker.
stop(

*self*
)

Description

Stop the running worker process if it exists. None

#### def main()

Description

Juju hook event dispatcher. None
