Version 0.8.0
Pre-releaseThe biggest change in this release is a major overhaul of the mechanics of transaction dependency linearization. This release retains almost 100% backwards compatibility and paves the way for new features in the near future. The only change that is not backwards compatible is that support for the struct tag redisType
has been removed. This was an undocumented and slightly broken feature so I would be surprised if anyone was using it.
_IMPORTANT_: With this release, relationships have been officially deprecated. Unfortunately, I've discovered bugs with the current relationships implementation and realized the abstraction layer is leaky. Unexpected errors can come up when using the feature in a real application. Right now, relationships still work exactly as they did in previous versions, but support will be removed entirely before version 1.0. See the README for more information.
Full Changelog:
- Completely rewrote the transactions abstraction layer
- Removed support for the
redisType
struct tag - Improved README, especially elaborating on limitations of current Sync implementation
- Deprecated relationships
- Added more benchmarks for queries
- Improved test performance slightly by only registering models once instead of registering and unregistering for each test
More About Transactions
The abstraction layer works like this: Each "transaction" consists of one or more "phases". Phases are executed as redis-style transactions via MULTI/EXEC. Each phase consists of one or more "commands", which are basically redis commands that are executed line by line inside of the MULTI and EXEC statements. Some phases depend on the results of other phases and therefore the order in which they are executed matters. For example, the first phase in a typical transaction will get all the ids for certain models with SMEMBERS (or ZRANGE if you specified an order in a query). The next phase will take all the ids and execute a series of HGETALL or HMGET commands to get the fields for each model.
The old implementation included a map[string]interface{}
inside every transaction, which was used to store and share data between phases. This was bad for two reasons: 1) It relied on using a string identifier for shared data (prone to potential typos) and 2) it was not type-safe and required a lot of type assertions when getting data from the map. It's easy to see how this could quickly get unwieldy. When it was time to execute a transaction, the old implementation used a greedy algorithm. It simply executed the phases that did not have dependencies first, then continued by executing any phases which were ready (i.e. those for which all the dependencies were already executed). This worked okay, but if there was a dependency cycle you wouldn't find out until after part of the transaction had already been executed, at which point you would get a nasty error message about how there are some phases that are still waiting for their dependencies to get executed. This was challenging to debug, and whenever there was an error, it left the database in a potentially unstable state since part of the transaction had been executed but not the rest (there is no "rollback" in redis-style transactions).
The newer implementation fixes all of these problems. Now, dependent phases are represented as nodes in a directed graph. There's a new method on phases called addDependency
which allows you to easily declare dependencies. References to shared data are passed around directly instead of using a map[string]interface{}
, which means they are type-safe. (I tried using channels to avoid sharing pointers, but the overhead had too much of an impact on performance). When it's time to execute the transaction, Zoom performs a topological sort on the graph to figure out the order in which to execute the phases. If there is a cycle, Zoom will detect it immediately and return an error instead of partially executing the transaction.
This change comes with a small performance hit (about 2-3µs for small transactions), which I suspect is due to allocating memory for the graph data structure (because it happens even in transactions with only one phase). I made the decision to move forward with the new implementation anyways, because it makes it easier to build new features, fix bugs, and manage the codebase in general. I'm going to try and reduce this performance hit in future versions.
I plan to do some work to make transactions even easier to use. Then, in future versions of Zoom the Transaction type and related functions/methods will be exported and accessible to users. The hope is that you can use transactions to easily manage relationships manually, write callbacks like AfterSave, BeforeSave, etc., and have more control and flexibility in general.
Although there's not many visible changes, this release involved a lot of work under the hood. It was quite a large undertaking and I'm excited about what it means for the future of Zoom!