Skip to content

Instantly share code, notes, and snippets.

@tedsuo
Last active August 29, 2015 14:01
Show Gist options
  • Save tedsuo/12ab24cb9840767e47ba to your computer and use it in GitHub Desktop.
Save tedsuo/12ab24cb9840767e47ba to your computer and use it in GitHub Desktop.
Migrating Go structs

Decouple Compare and Swap from the serialized Value

if we modify storeadapter#compareAndSwapByIndex to return the new index, we can use this mechanism for our lifecycle/state machines:

type StoreAdapter interface {
  CompareAndSwapByIndex(prevIndex uint64, newNode StoreNode) (newIndex uint64, err error)
}

This would allow our state machines to continue, even if the values changed (provided all versions of the model have a StoreIndex field that we can use). Doesn't solve the issue of our processes expecting the data to be in a different format that the model they currently have, but it does prevent model changes from messing up the state machine.

Migrating serialized values in etcd

For BBS models (currently LRP and Tasks), the BBS can do a lazy migration whenever it deserializes data from etcd. If the version is older than the current version, it runs the appropriate migration function and returns the expected latest version. To do this, we would need to move to versioned types, so you would have a TaskV1 and a TaskV2. Something like:

type TaskV1 struct {
	Version int
	Name string
}

type TaskV2 struct {
	Version int
	FirstName string
	LastName string
}

var ErrVersionToDamnHigh = errors.New("version is higher than current")

func unmarshalTaskV2(jsonBytes []byte) (TaskV2, error) {
	taskV2 := TaskV2{}

	err := json.Unmarshal(jsonBytes, taskV2)
	if err != nil {
		return migrateV1toV2(jsonBytes)
	}

	if taskV2.Version == 2 {
		return taskV2
	}

	if taskV2.Version < 2 {
		return migrateV1toV2(jsonBytes)
	}

	if taskV2.Version > 2 {
		return TaskV2{}, ErrVersionToDamnHigh
	}
}

func migrateV1toV2(jsonBytes []byte) (TaskV2, error) {
	taskV1, err = unmarshalTaskV1(jsonBytes)
	taskV2 := TaskV2{}
	// do whatever migrating means
	return taskV2
}

Migrating serialized values sent of HTTP

Besides etcd, we also have some models that are used in client-server communication. For example, the REST api on the executor. For situations like this, we control both the client and the server, and can use headers to indicate the version. The client sends a X-Data-Version header, and the server can do migrations based on what version it sees, or reject the request if the version is too high or too low.

Use tests to ensure migrations happen when needed

In general, the solution is to have versioned types for ANY type that must be serialized. So, the moment we add a json "key" annotation to a struct, we need to also version the type and add a Version field to it. Then, we add tests to the type which warn developers that they are NOT allowed to "fix" the test, but instead must go back and version the type, create a migration, and update all the call sites that uses the model.

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