This email is more of a preliminary brain dump than it is a proposal. It's
just to work out some of the requirements and design tradeoffs that I've
been mulling over in the last few weeks, trying to get to a point where
detailed API design can begin. Please feel free to jump in with questions
or comments about any of it. Thanks.
Background and Requirements Overview
------------------------------------
We want to be able to dump user data from a Chandler repository into a
stable external format, and be able to reload the resulting backup into the
same or newer versions of Chandler. The intended uses are for backups and
to support evolution of both the repository implementation and our domain
models without requiring users to discard their data.
This overall strategy was chosen after a discussion during the PyCon
sprints; a writeup of the analysis can be found here:
http://lists.osafoundation.org/pipermail/chandler-dev/2006-February/005301.html
By "stable external format", I mean a format that does not change
significantly from one Chandler release to the next, and which allows for
version detection of the format itself, as well as providing version and
schema information for the parcels whose data is contained in the format.
As I see it, there are several forces to be resolved in the design, most of
which compete with all of the others:
Ability to perform upgrades flexibly
In the common case, schema upgrades will consist mostly of simple
additions or renames. But in uncommon cases, entire families of objects
may be restructured, or data might be moved from one Kind to another.
Implementation Complexity
We don't have the resources to do original research in schema
evolution techniques, nor can we afford to essentially duplicate the
existing repository implementation in some other form. If possible, the
actual data format shouldn't reinvent wheels either.
API Complexity
In principle, we could have have a simple API by just asking each
parcel to write its data to a provided file. However, this is just asking
each parcel author to invent their own format while revisiting the same
tradeoffs. Ideally, our API should allow a parcel developer to write a
minimum of code to support simple upgrades, and it should make complex
upgrades possible.
Effective Support for Parcel Modularity
Chandler isn't a monolithic application, and parcels can be upgraded
independently of one another. Effective dump-and-reload thus requires that
parcels' data be managed in a way that allows multiple upgrades to occur
during the same load operation, and that dumps occur in a way that allows
each parcel's schema or version information to be recorded
separately. Ideally, this modularity would extend to the data as well as
the schema, so that a single parcel's data could be backed up or restored,
subject to the parcel's dependencies. (Meaning that we could perhaps at
some point allow upgrading a single parcel's schema without requiring a
complete dump and reload.)
Performance
Dumping should be fast. Reloading shouldn't be slow for simple common
cases, but it's okay to be slow if a complex schema change occurs.
Design Implications
-------------------
There are several places where these forces resolve to fairly
straightforward preliminary conclusions about how the system should work:
* The external format should deal in relatively simple data structures
composed of elementary values (of a relatively small number of types)
arranged in records. Blobs should also be supported. (Forces: reduce
implementation complexity, increase dump performance)
* The format shouldn't use nesting structures that would require temporary
storage of extremely large sequences. (Forces: reduce implementation
complexity, increase load performance)
* Notifications of all kinds (including onValueChanged) must be disabled
during a load operation -- and that includes refresh() notifications in
other views; load operations should be globally exclusive, with no other
activity taking place. Which also means no repository-based UI components
being active. In other words, "Chandler as we know it" *can't be running
during a load operation*.
* To allow for simple upgrades, parcels should be allowed access to the
stream of records being loaded, so as to transform them directly into
records that match the parcel's current schema.
* To allow for complex upgrades, parcels should be given "before" and
"after" hooks. This allows them to use the repository itself to store
working data during a complex schema change. Without this feature, it
would be necessary to provide query facilities in the dump/reload system
itself, increasing implementation complexity. But with this feature,
simple upgrade reloads and non-upgrade reloads can be fast, and only very
complex upgrades pay a penalty in performance and implementation complexity.
But the list of things that *aren't* so neatly resolved is bigger.
For example, what dumping order should be used? The modularity requirement
argues in favor of dumping on a per-parcel basis, but this means that
annotated kinds will be scanned multiple times, once for each parcel that
annotates the kinds in question. So dumping performance seems to argue
that it would be better to just walk the entire repository and write data
as you find it. (At least, if you're going to be dumping most of the
repository contents, anyway.)
Reloading performance, on the other hand, seems to argue that the data
should be broken up by record types, in order to avoid repeated dispatching
overhead. That is, if you iterate over a sequence of identical records,
you only have to look up the filtering code once, and you can even write it
as a generator to avoid extra call overhead. (Of course, these performance
issues could easily be dominated by the cost of writing the data to the
repository.)
Hm. Actually, I've just thought of a way to get the advantages of both
approaches, by writing interleaved records at dump time and reading them in
an interleaved way at reload time. Making it work for iterators would be a
pain, but doable. Okay, so scratch that issue off the "unresolved" list. :)
Using iterators does have another big advantage, though. If a parcel can
provide an iterator to do record transformation (simple schema upgrades),
the iterator can also run code at the beginning and end of the process --
which means it can also do complex upgrades that need setup and cleanup steps.
And writing these steps at the beginning and end of a loop that processes
loaded records is a simple and "obvious" way to do it, without having to
learn multiple APIs for upgrading the schema. The only tricky bit in the
API is that we'd have to guarantee relative ordering for these
transformation functions, so that the parcel author knows what order the
iterators will be called in. But that's mostly an implementation detail.
So, if we could reduce every item to records, then each Item subclass would
just need a 'load_records()' classmethod that iterated over an input and
yielded transformed (or untransformed) records. The default implementation
of this classmethod would simply yield untransformed records as long as
their schema matched the current schema, and raise an error otherwise.
This concept appears like it would work for an overall repository dump and
reload of items by themselves. It does not address certain issues
regarding many-to-many and ordered relationships (which I'll get to later),
nor does it handle the problem of upgrading parcel-specific data structures
(e.g. block trees and copies thereof).
The issue with parcel-specific data structures is that these are not things
described by the parcel's own schema. For example, if you have a tree of
blocks, they're going to be described by block framework schemas. So,
there seems to be a need for a post-reload hook that gets called in a
parcel to allow fixup of such structures. Perhaps the simplest way to
support that would be to invoke installParcel() again, with oldVersion set
to the reloaded version.
These installParcel() recalls would have to happen in an order that ensures
it has been already called for dependent parcels. That is, if parcel foo
depends on parcel bar, then bar's installParcel() must be called before foo's.
(Note that this means we *must* eliminate any remaining dependency cycles
between parcels in order to implement a viable dump/reload system.)
Storing Relationships
---------------------
In order to "flatten" items into simple data records, references to other
objects have to be reduced to elementary types or records thereof. This
means, for example, that inter-item references might need to be reduced to
a UUID.
In the simplest case of one-to-one relationships, this is straightforward
since it's just representing the attribute as a UUID. Even simple
one-to-many relationships are easily handled by representing only the "one"
side of the relationship in the records, and allowing the "many" side to be
rebuilt automatically via the biref machinery when the records are reloaded.
There is a problem with this approach, however, because all birefs are
currently *ordered* collections, even in cases where the order is
meaningless. I had hoped during the schema API implementation last year
that we could migrate to using ``schema.Many()`` in places where an ordered
collection was unnecessary, but I didn't realize at the time that "set"
cardinality in the repository could not be part on a bidirectional
reference, so ``schema.Many()`` has mostly gone unused.
Why is this important? Because external representation of data as a stream
of records requires additional fields or records to reconstruct the
sequence in a relationship. Either an additional field is required to
store a key that indicates the order, or there has to be a sequence of
records whose sole purpose is to represent the ordering. (This is
especially complex in the case of order-preserving many-to-many relationships!)
So, a key step in getting our schema ready for dump-and-reload support is
going to be examining our current use of schema.Sequence to see whether
these use cases actually require order-preservation. In many cases, we are
actually using indexes based on other attribute values, so the unindexed
order isn't necessary and it would be redundant to write it out in a
dump. We currently have about 120 Sequence attributes that would need to
be reviewed.
Since we have only around 3 uses of ``schema.Many()``, I would suggest we
create a new descriptor to use in these cases, to free up the use of
``schema.Many`` for ``Sequence`` attributes that don't really need to be
sequences. That would leave the following as possible relationship types,
(ignoring mirror images):
* One - Many
* One - One
* Many - Many
* One - Sequence
* Many - Sequence
* Sequence - Sequence
One-to-many and one-to-one relationships can be represented in external
form without introducing any new fields or records; records on the "one"
side would just record the identity of the referened object.
Most of the other relationships would require an extra series of records to
be written out, each record listing the identity of the objects on either
side of the link. In the One-Sequence and Many-Sequence cases, the order
of the records would indicate the order of the links on the "Sequence" side
of the relationship.
Sequence-Sequence relationships, however, are unstable, in that there is no
simple way to read and write them without including data to indicate the
sequence on both sides, and having a cleanup pass to fixup the loaded
sequences. (Or by having special support from the repository to allow each
side to be loaded indepdently without the normal automatic linking.)
Thus, if possible, I'd like to abolish Sequence-to-Sequence relationships,
allowing only the other five kinds of bidirectional relationships to
exist. A sequence-to-sequence relationship is inherently unstable given
the nature of birefs, anyway. Even if you're adding things to one side in
a particular controlled order, the other side likely won't be in a
meaningful order of its own. That we have these relationships in the
schema now is largely due to "many" and "sequence" being spelled the same
way (i.e. ``schema.Sequence``) because the repository's current
implementation doesn't offer a non-order-preserving cardinality that works
as half of a biref.
This would require a schema review, but I think it's going to be important
to make this distinction in the schema anyway, as it ultimately gives more
implementation flexibility to the repository for future performance
improvements, if we only have to implement sequences when sequences are
really required. (Today's repository will not care, of course, so the
distinction will be purely for documentation of intent and for possible
simplification of the dump/reload process.)
Note that I've been making an assumption in all of the above that records
would not contain any sequences or complex data structures themselves; but
this assumption needn't apply to small "many" or "sequence" attributes. It
could be an optimization to write such links inline, but it would require
hint information of some kind in the schema to suggest which side is better
to write this information out on. I'm somewhat inclined against this
approach, though, because we will also have to support large sequences
(e.g. from UI-level item collections) and I'd just as soon not have two
different implementations.
But that's more of an API distinction; the API can to some extent represent
things as indepdendent records, even if the underlying storage format
interleaves the data to some extent. And conversely, the API could
transform a non-interleaved format to an interleaved one, albeit at the
cost of additional memory.
Wrap-up -- for now
------------------
At this point I haven't covered much actual API detail, or anything at all
about the actual external format. I don't actually care much about the
external format, since it's not a requirement that it be processed by other
programs, and parcel writers will never see it directly. The API will only
expose streams of records of elementary types, and provide a way for parcel
writers to transform individual records as the streams go by, and to do
pre- and post-processing on the repository contents.
There's still a lot of work to be done to take these high-level thoughts
and turn them into a concrete API proposal, but at this point I'd like
input and feedback on the points I've presented so far, in order to make
sure that the next steps are going in the right direction.
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
Open Source Applications Foundation "chandler-dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/chandler-dev