Snap epochs

Epochs enable snap developers to control how users receive a new application release when an application’s data format becomes incompatible with older versions of the application.

Epochs require snap version 2.38+.


Defining an epoch

Applications and their data formats are constantly evolving, and this requires applications to periodically break data compatibility with older versions. When this happens, applications and users often need to carefully manage data migration from one version to another, and this is where epochs can help.

By default, snaps have an epoch of ‘0’. When a new version breaks data compatibility with this old version, incrementing the epoch in the new release stops those old users automatically refreshing to the new version. To do this, add the following to your snap’s snapcraft.yaml:

epoch: 1

The revisions with the new epoch are invisible to users on the original epoch: 0; conversely, the old epoch is invisible to new users who always get the latest epoch with a fresh installation. You can still push updates to users of the old epoch by specifying epoch: 0 (or omitting the epoch) in its snapcraft.yaml, while pushing updates to the new version using epoch: 1. It’s a little like having subchannels.

Migrating to a new epoch

Epochs alone track the format used but do not automatically migrate user data between application versions. This data is still copied, as it normally would, but snapd isn’t aware of its format, or how to migrate the data from an old epoch to a new one; the snap developer needs to manage and implement this process (e.g. run database migrations, etc.).

snapd does not track which users have accessed a snap, or whether a user’s home directory is still accessible, or even if a user’s home directory is located in /home/<user>. Similarly, epochs cannot automatically safeguard migrating version-dependent data in $SNAP_COMMON because, if a refresh fails, there’s no guaranteed way to retrieve the old data; snap revert will not bring the data back

Your own data migration strategy could use hooks within your snapcraft.yaml, or rely on functionality within your application at run-time.

When you have confidence in a migration process, you can then label the new epoch with an asterisk (*) to indicate its compatibility with old data versions.

For example, to create a snap that says it knows how to read both epoch 0 data and epoch 1 data, but only write epoch 1 data, you add the following to your snapcraft.yaml:

epoch: 1*

The resulting snap acts as a bridge between epoch 0 and epoch 1. Users of the old version migrate automatically when they refresh. They will then upgrade to any newer releases in epoch 1, just like any new user. You can use revert to switch back to releases from a previous epoch, discarding any data changes done by the latest revision.

If users need to test data compatibility with the new epoch version, and the snap is not a confinement: classic snap, you can install both epoch 0 and epoch 1 snaps at the same time using Parallel installs. This allows your users to migrate only when they have the confidence to do so.

A big advantage with automatic migration is that the data transformation process needs to be supported only for a limited number of revisions. If a user neglects to update their system across epochs, for example, when they eventually refresh, they will be stepped through each epoch: 0 to 1*, 1 to 2*, and 2 onwards, for instance, by performing the migration process for each step as necessary. You don’t need to push old migration code to new versions of your application.

Epoch transitions

Epoch transitions

The following table illustrates which revision (R) and epoch (E) a user will receive when refreshing an application from stable and candidate channels:

stable
releases
candidate
releases
stable
for user 1
candidate
for user 2
candidate
for user 3
1 R1E0 - R1E0 R1E0 -
2 R2E0 - R2E0 R2E0 -
3 - R3E1 R2E0 R2E0 (!) R3E1
4 R4E0 - R4E0 R4E0 (!) R3E1
5 R5E1 - R4E0 R4E0 R3E1 (!)
6 - R6E1* R4E0 R6E1 R6E1
7 R6E1* - R6E1 R6E1 R6E1
8 - R7E1 R6E1 R7E1 R7E1
(!) is used to show the influence of epochs on locally installed revisions.

Internal read/write configuration

Internally, snapd maintains a list of which revisions of a snap can read and write to each epoch. You can set these from snapcraft using passthrough. For example, 1* is equivalent to:

passthrough:
  epoch:
    read: [ 0, 1 ]
    write: [ 1 ]

There are rules about what can be in read and write, but in general, you should not need to use this syntax at all. We’re mentioning it here because the store only supports the extended format, so you’ll see it if you query the store directly.

2 Likes

Should step 6 of the second user installing from candidate be indicating R6, not R4? I mean they chose to track candidate, so even though they were forwarded to stable initially they should get the candidate channel as soon as it is epoch-compatible, i.e. when R6 is in candidate.

You’re absolutely right! Good catch - I’ve updated the table to fix the wrong revision number. Thank you!

1 Like

I think this point could be expanded a bit: " Your own data migration strategy could use hooks within your snapcraft.yaml , or rely on functionality within your application at run-time."

That is, some further explanation of the mechanics of migrating actual data might be useful.

Note also that hooks are not “within” your snapcraft.yaml: they are separate files in the snap.

Why do steps 5 and 6 not give user 1 R5? There is no instance that R5 will be delivered to either of the users according to the table…?

Edit: oh I see the R5 isn’t a backwards compatible (i.e. E1* the asterisk being important - this is a strange and confusing syntax but it is what was chosen for the snapcraft.yaml I guess - I don’t like it)

This table is very hard to understand what you’re trying to convey. I think there needs to be a better way to represent this.

Also, why doesn’t user 3 get R6 in step 6 - the table shows they’re held back to R3 which is E1 the same as R6 and both are in the candidate channel?

Edit: again this appears to be a E1* vs E1 difference?

Thanks for your comments, and you’re right - it is very difficult to understand. I’ll try and think of a better way of representing all this.

Maybe a flow-diagram would be better? Or more specifically three separate flow diagrams, one for each user? It would also be helpful to have like a footnote indicator to explain why each step chose a particular revision in each step as a separate description.

1 Like

I’m not sure what the following sentence is trying to tell me. I understand that snapd doesn’t track this, but what does that mean for me as a snap maintainer?

snapd does not track which users have accessed a snap, or whether a user’s home directory is still accessible, or even if a user’s home directory is located in /home/<user>.

This sentence is clear, however.

Similarly, epochs cannot automatically safeguard migrating version-dependant data in $SNAP_COMMON because, if a refresh fails, there’s no guaranteed way to retrieve the old data; snap revert will not bring the data back

Either row 6 is wrong or I don’t understand it at all for user 3. If user 3 installed it from candidate, shouldn’t line 6 make it go to R6 and not line 7?

If it’s just because I don’t understand it could someone EL5 it to me? :slight_smile: Thanks!

You are absolutely right about this. Thanks so much for letting us know. I’m trying to work out a better way to present this information, but it certainly didn’t help with a mistake in the table!!

Fwiw, I think the table is fine. I didn’t have any trouble with it (except for the mistake being confusing).

1 Like