1 Introduction
2 Use case and requirement analysis
Data source | Examples | Web URL |
---|---|---|
SQL databases | MySQL | |
PostGreSQL | ||
Microsoft SQL Server | ||
Virtualization platforms | VMware VCenter | |
OpenStack | ||
Red Hat Enterprise Virtualization | ||
Enterprise architecture management tools | IteraPlan | |
Mega | ||
Enterprise Architect | ||
Cloud Computation Platforms | Amazon EC2 | |
Microsoft Azure | ||
Google Cloud Compute Engine | ||
Container orchestration mechanisms | Google Kubernetes | |
Docker Swarm |
2.1 Deriving requirements from the context
2.2 Deriving features from requirements
3 Solution overview
4 Solution part I: ChronoDB
4.1 Formal foundation
-
Pure-Timeslice Query Given a point in time (e.g., date and time), find all keys that existed at that time.
-
Range-Timeslice Query Given a set of keys and a point in time, find the value for each key which was valid at that time.
-
Pure-Key Query Given a set of keys, for each key find the values that comprise its history.
Capability | Realization in formalism |
---|---|
Pure-Timeslice | Equivalent to keyset operation |
Range-Timeslice | One get operation per given key |
Pure-Key | One history operation per given key |
4.2 Implementation
4.2.1 Implementing the matrix
Order | Temporal key | Key string | Timestamp |
---|---|---|---|
0 | a@0123
| a | 123 |
1 | a@0124
| a | 124 |
2 | a@1000
| a | 1000 |
3 | aa@0100
| aa | 100 |
4 | b@0001
| b | 1 |
5 | ba@0001
| ba | 1 |
aa@0050
(which is not contained in the data), searching for the next-lower key produces a@1000
. The key string in this temporal key (a
) is different from the one which was requested (aa
). In this case, we can conclude that the key aa
did not exist up to the requested timestamp (50), and we return null
instead of the retrieved result.4.2.2 Branching
4.2.3 Caching
4.2.4 Incremental commits
commitIncremental()
. This writes the first batch (timestamp 2 in Fig. 9) into the database and releases it from RAM. However, the now timestamp is not advanced yet. We do not allow other transactions to read these new entries, because there is still data left to insert. We proceed with the next batches of data, calling commitIncremental()
after each one. After the last batch was inserted, we conclude the process with a call to commit()
. This will merge all of our changes into one timestamp on disk. In this process, the last change to a single key is the one we keep. In the end, the timestamps between the first initial incremental commit (exclusive) to the timestamp of the final commit (inclusive) will have no changes (as shown in timestamps 3 and 4 in Fig. 9). With the final commit, we also advance the now timestamp of the matrix and allow all other transactions to access the newly inserted data. By delaying this step until the end of our operation, we keep the possibility to roll back our changes on disk (for example in case that the process fails) without violating the ACID properties for all other transactions. Also, if data generated by a partially complete incremental commit process is present on disk at database start-up (which occurs when the database is unexpectedly shut down during an incremental commit process), these changes can be rolled back as well, which allows incremental commit processes to have “all or nothing” semantics.4.2.5 Supporting long histories
Time range | Chunk number |
---|---|
\([0 \ldots 300]\)
| 0 |
\([301 \ldots 1000]\)
| 1 |
\([1001 \ldots \infty ]\)
| 2 |
4.2.6 Secondary indexing
ChronoIndexer
interface. It defines the index(Object)
method that, given an input object, returns the value that should be put on the secondary index. Each indexer is associated with a name. That name is later used in a query to refer to this index. The associated query language provides support for a number of string matching techniques (equals, contains, starts with, regular expression...), numeric matching (greater than, less than or equal to...) as well as Boolean operators (and, or, not). The query engine also performs optimizations such as double negation elimination. Overall, this query language is certainly less expressive than other languages such as SQL. Since ChronoDB is intended to be used as a storage engine and embedded in a database frontend (e.g., a graph database), these queries will only be used internally for index scans while more sophisticated expressions are managed by the database frontend. Therefore, this minimalistic Java-embedded DSL has proven to be sufficient. An essential drawback of this query mechanism is that the number of properties available for querying is determined by the available secondary indices. In other words, if there is no secondary index for a property, that property cannot be used for filtering. This is due to ChronoDB being agnostic to the Java objects it is storing. In absence of a ChronoIndexer
, it has no way of extracting a value for an arbitrary request property from the object. This is a common approach in database systems: without a matching secondary index, queries require a linear scan of the entire data store. When using a database frontend, this distinction is blurred, and the difference between an index query and a non-index query is only noticeable in how long it takes to produce the result set.# | Index | Branch | Keyspace | Key | Value | From | To |
---|---|---|---|---|---|---|---|
1 | name | master | default | e1 | “john” | 1234 |
\(\infty \)
|
2 | name | master | default | e2 | “john” | 1234 | 5678 |
3 | name | master | default | e3 | “john” | 1234 | 7890 |
4 | name | master | default | e2 | “jack” | 5678 |
\(\infty \)
|
ChronoIndexer
. “From” and “To” express the time window in which a given row is valid. Any entry that is newly inserted into this table initially has its “To” value set to infinity (i.e., it is valid for an unlimited amount of time). When the corresponding entry in the primary index changes, the “To” value is updated accordingly. All other columns are effectively immutable.4.2.7 Transaction control
5 Solution part II: ChronoGraph
5.1 Apache TinkerPop
null
value. Instead of assigning null
to a property, it is recommended to delete the property on the target graph element entirely.5.2 ChronoGraph architecture
5.3 Data layout
5.3.1 Partitioning: the star graph format
5.3.2 Key–value layout
TinkerPop | Record | Record contents |
---|---|---|
Vertex | VertexRecord | id, label, |
PropertyKey \(\rightarrow \) PropertyRecord | ||
In: EdgeLabel \(\rightarrow \) EdgeTargetRecord | ||
Out: EdgeLabel \(\rightarrow \) EdgeTargetRecord | ||
Edge | EdgeRecord | id, label, |
PropertyKey \(\rightarrow \) PropertyRecord | ||
id of InVertex, id of OutVertex | ||
Property | PropertyRecord | PropertyKey, PropertyValue |
— | EdgeTargetRecord | id of edge, id of other-end Vertex |
String
. The PropertyValue in the PropertyRecord is assumed to be in byte array form. An arrow in the table indicates that the record contains a mapping, usually implemented with a regular hash map. An element that deserves special attention is the EdgeTargetRecord that does not exist in the TinkerPop API. Traversing from one vertex to another via an edge label is a very common task in a graph query. In a naive mapping, we would traverse from a vertex to an adjacent edge and load it, find the id of the vertex at the other end, and then resolve the target vertex. This involves two steps where we need to resolve an element by ID from disk. However, we cannot store all edge information directly in a VertexRecord, because this would involve duplication of all edge properties on the other-end vertex. We overcome this issue by introducing an additional record type. The EdgeTargetRecord stores the id of the edge and the id of the vertex that resides at the “other end” of the edge. In this way, we can achieve basic vertex-to-vertex traversal in one step. At the same time, we minimize data duplication and can support edge queries (e.g., g.traversal().E()
in TinkerPop), since we have the full EdgeRecords as standalone elements. A disadvantage of this solution is the fact that we still need to do two resolution steps for any query that steps from vertex to vertex and has a condition on a property of the edge in between. This trade-off is common for graph databases, and we share it with many others, e.g., Neo4j. We will discuss this with a concrete example in Sect. 5.4.5.4 Versioning concept
5.5 TinkerPop compatibility and extensions
5.5.1 TinkerPop API compliance
5.5.2 TinkerPop extensions
Vertex
and Edge
classes) is unaware of the versioning process (as indicated in Sect. 5.4), we are left with one possible extension point, which is the Graph
interface itself. In order to offer queries access to timestamps other than the head revision, we need to add a method to open a transaction on a user-provided timestamp. By default, a transaction in TinkerPop on a Graph
instance g
is opened without parameters. We expand the transaction class by adding several overrides which accept the desired target branch and version. Using these additional overrides, the user can decide the java.util.Date
or java.lang.Long
timestamp on which the transaction should be based, as well as the branch to operate on. This small change of adding an additional time argument is all it takes for the user to make full use of the time travel feature, the entire remainder of the TinkerPop API, including the structure elements and the Gremlin query language, behave as defined in the standard. Opening a transaction without parameters defaults to opening a transaction on the latest version of the master branch, which is also in line with the TinkerPop API specification.Graph
implementation. These methods allow access to the history of any given edge or vertex. The history is expressed by an Iterator
over the change timestamps of the element in question, i.e., whenever a commit changed the element, its timestamp will appear in the values returned by the iterator. The user of the API can then use any of these timestamps as an argument to g.tx().open(...)
in order to retrieve the state of the element at the desired point in time. The implementation of the history methods delegate the call directly to the underlying ChronoDB, which retrieves the history of the key–value pair associated with the ID of the given graph element. This history is extracted from the primary index, which is first sorted by key (which is known in both scenarios) and then by timestamp. This ordering allows the two history operations to be very efficient as only element ID requires a look-up in logarithmic time, followed by backwards iteration over the primary index (i.e., iteration over change timestamps) until a different ID is encountered (c.f. Table 3).Graph
implementation. These methods (one for vertices, one for edges) accept time ranges and grant access to iterators that return TemporalKeys. These keys are pairs of actual element identifiers and change timestamps. Just as their element-specific counterparts, it is intended that these timestamps are used for opening transactions on them in order to inspect the graph state. Combined calls to next()
on it1
and it2
will yield the complete list of changes upon iterator exhaustion. Analogous to their element-specific siblings, these methods redirect directly to the underlying ChronoDB instance, where a secondary temporal index is maintained that is first ordered by timestamp and then by key. This secondary index is constructed per keyspace. Since vertices and edges reside in disjoint keyspaces, these two operations do not require further filtering and can make direct use of the secondary temporal index.5.6 Transaction semantics
null
value and should never throw a ConcurrentModificationException
, but details regarding the visibility of changes made by other, concurrent transactions are unspecified.5.7 Functionality and usage implications
5.8 Conflict resolution
firstname
property to John
, while the other sets the lastname
property to Doe
, then the conflict is resolved by accepting both changes (the resulting vertex will have a firstname
of John
and a lastname
of Doe
). Only if both transactions modify the same property on the same graph element, then a true conflict occurs. Here, we employ the same strategy as for graph elements: if either side of the conflict is a deletion, the deletion wins. If neither side is a deletion, then the last writer wins. For example, if one transaction sets firstname
to John
and a concurrent transaction sets firstname
to Jack
on the same graph element, then the conflict is resolved by using the firstname
value from the transaction which was committed later in time.5.9 Limitations and drawbacks
6 Solution part III: ChronoSphere
ChronoSphere
instance manages a number of named Branch
es (with master as the predefined one) [R4], and each Branch refers to its origin
(recursively). Each Branch contains any number of Version
s [R8], which in turn contain a user-defined (Ecore-based) Metamodel
and an InstanceModel
, which is a collection of EObject
s that adhere to the EClass
es in the Metamodel. A ChronoSphere instance can then create Transaction
s [R6] on a given Version by starting them on a transactionTimestamp
, which is usually obtained from a user-provided java.util.Date
. This is a fairly common setup for versioning- and branching-enabled model repositories. A detail deserving special attention is the fact that a Version and a Metamodel are bound to each other in a one-to-one relationship. This is a requirement for metamodel evolution [R3], which we will discuss in Sect. 6.4.6.1 Graph layout
6.2 EQuery
EObject
s in our model. We then narrow the search space by restricting the elements to have an EClass
named “Application”. We then filter the EAttribute
s of the EObject
s and look for Applications with a “name” equal to “Mail Server”. Then, we navigate along the “runsOn” EReference
using the familiar eGet(...)
syntax. The result is a stream of arbitrary objects21 which needs to be filtered to include only EObject
s. Finally, we convert the stream into a Set
for further processing. All of this is expressed within regular Java code.
and(...)
step. This step will filter out all EObjects where at least one of the provided subqueries does not produce any object. Each subquery starts at the same object, which is the one which was passed into the and
step. In our example, we pass in a cluster object into the and
step, and check if it runs on at least one physical and at least one virtual machine.
EReference
to the virtual machines. For every application where this navigation step produced a valid result (i.e., application.eGet("runsOn")
is non-empty), we check if at least one of the resulting EObjects is of type Virtual Machine. If there is such an instance, we navigate back
to the previously marked “apps” position. This produces a stream that only contains EObject
s that have at least one target for the “runsOn” EReference
which is a Virtual Machine. It is important to note here that the back
step will only be performed if at least one element passed the previous step. This style of backtracking works in arbitrary distances between query steps, and the programmer can define an arbitrary number of (uniquely) named steps to jump back to. They are useful in cases where the required filter condition is not on an element itself but on its neighbors, or in order to avoid long backtracking navigation. A final detail worth mentioning in Listing 3 is that we first retrieve the EClasses from the EPackage. In most places, the EQuery API allows the programmer to choose between passing a name (EClass name, EAttribute name...) or a variable of the appropriate type. Generally speaking, passing in the metamodel element directly is more efficient than specifying it by name. In all further listings, we will not include the variable definitions.6.2.1 EQuery validation and type safety
eGet(“name”)
is always correct according to the Java compiler, even if the EAttribute name
does not exist in the current metamodel. Furthermore, the Java compiler cannot infer that eGet(“name”)
will produce an element of type String
; all it knows is that it produces some result Object
. For such cases, EQuery provides content filtering and casting methods (e.g., asEObject()
, asNumber()
...) which first apply an instanceof
filter and then downcast the passing elements.ocl.evaluate(literal)
method call will always be of type Object
which then needs to be downcast to the expected type. As both the content of the OCL string literal as well as the downcast itself can be changed freely without causing type errors from the Java compiler, we argue that this method does not provide full type safety for application developers. This is not limited to OCL: all query languages which rely on string literal representation (such as SQL and HQL) also suffer from the same issue.6.3 Metamodel evolution
EAttribute
to an existing EClass
, the existing instances are still valid (provided that the attribute is not mandatory), they just have no value set for the new attribute. Other basic examples include the addition of new EClass
es or increasing the multiplicity of an EAttribute
from multiplicity-one to multiplicity-many. However, far more complex examples exist as well, and in many cases, fully automatic and deterministic instance co-adaptation is not possible. Cicchetti et al. refer to such cases as unresolvable breaking changes [9]. For instance, we consider a metamodel that contains an EClass
A. The next version of the same metamodel does not contain A anymore, but a new EClass
named B instead. Even though there are algorithms for model differencing [16, 40, 72], in the absence of unique identifiers (e.g., UUIDs) and change logs we cannot tell if A was removed and B was added, or if A was merely renamed to B. In the first case, we would have to delete all instances of A, in the second case we would need to migrate them to become instances of B. This basic example shows that instance co-adaptation requires semantic information about the change, which is only available to the application developer. For this reason, ChronoSphere provides an API for managing metamodel evolution with instance co-adaptation [R3]. Rose et al. provide a summary of related approaches [60]. This in-place transformation approach is in line with Wimmer [75] and Meyers [48], with the notable difference that we propose a Java API instead of ATL processes or DSLs [35, 59]. The concept is also similar to the Model Change Language [49]. This API offers three different modes of operation:-
Changes without need for instance adaptationThis kind of evolution is intended for the most basic changes that do not require any kind of instance co-adaptation. Examples include adding
EClass
es, addingEAttribute
s, or increasing feature multiplicities from one to many. This category is also known as non-breaking changes [9]. The developer only provides the new version of the metamodel and loads it into ChronoSphere, which will create a new version in the history. -
One-to-one correspondenceWhen instance co-adaptation is required, a common case is that each
EObject
from the old model will still correspond to (at most) oneEObject
in the new model. Examples for changes in this category include the renaming ofEClass
es andEAttribute
s. For such cases, the ChronoSphere metamodel evolution engine provides the developer with a predefined evolution process and a predefined element iteration order. The developer implements an Incubator that is responsible for either migrating a givenEObject
to match the new metamodel, or deleting it if it is obsolete. The Incubator is specific to a given source and target metamodel and contains the semantics and domain-specific constraints of the migration, expressed in Java source code. -
Generic adaptationIn more complex cases, a one-to-one correspondence of elements can no longer be established, for example when an
EClass
is refactored and split up into two separate classes. In such cases, ChronoSphere provides a generic Evolution Controller interface that is in full control over the instance co-adaptation. It receives the migration context, which provides utility methods for querying the old and new model states. The migration process as well as the iteration order of elements are defined by the implementation of the controller. For that reason, implementing an evolution controller is the most powerful and expressive way of defining a migration, but also the most technically challenging one that entails the highest effort for the developer. Just like the incubators from the one-to-one correspondence case, such migration controllers are specific to a given source and target metamodel version.
EAttribute
has been deleted), we would not be able to guarantee traceability anymore. Consequently, we would introduce a considerable threat to the validity of audits. By storing a new metamodel alongside the co-adapted instance model, we restrict the impact of a metamodel evolution to a single version (e.g., the version that simultaneously introduces \(m_{4}\) and \({mm_{2}}\) in Fig. 18) and can still guarantee traceability in the remaining sections of our data. As we will discuss in the remainder of this section, in our approach, we can guarantee traceability even across metamodel evolutions.
6.4 Advantages and limitations of the incubator approach
-
Removal of classes
-
Addition of new classes
-
Renaming of any metamodel element
-
Any changes to EAttributes and EReferences
-
Any combination of changes listed above
6.5 Transaction and versioning concepts
7 Industrial case study
8 Performance evaluation
-
CDO is widely considered to be the “gold standard” in model repositories.
-
In the spectrum of competing solutions, CDO is closest to ChronoSphere with respect to features.
-
CDO is based on a relational store, which allows for a discussion of individual advantages and drawbacks when comparing it to our graph-based approach.
8.1 Model insertion
8.2 Assets By Name
8.3 Root cause analysis
closure
statement in both languages to navigate the transitive paths. We require this statement because the path length is unknown—a Virtual Machine may run on a Cluster which in turn is deployed on Virtual Machines. Such a query is very difficult to express in SQL and would require recursive JOIN operations. However, since CDO resolves OCL queries via in-memory iteration, it circumvents this issue.
8.4 Impact analysis
8.5 Threats to validity
CDO (H2) | CDO (PGSQL) | ChronoSphere | |
---|---|---|---|
Model Loading | 13,404.5 | 83,915.8 | 46,077.0 |
Root Cause Analysis | 1509.6 | 1504.2 | 636.3 |
Impact Analysis | 33,155.0 | 32,907.9 | 12.5 |
Assets By Name | 23,292.3 | 19,470.0 | 1517.4 |
8.6 Benchmark summary
9 Discussion and related work
9.1 Related Key-Value-Store versioning solutions
9.2 Related graph versioning solutions
9.3 Related repository solutions
Technology | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | Deployment | Persistence |
---|---|---|---|---|---|---|---|---|---|---|---|
Eclipse CDO | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | On Premise | SQL/Documents | ||
MORSA | ✓ | On Premise | Documents | ||||||||
EMFStore | ✓ | ✓ | ✓ | ✓ | On Premise | XML Files | |||||
MagicDraw Teamwork Server | ✓ | ✓ | ✓ | On Premise | XML Files | ||||||
MagicDraw Teamwork Cloud | ✓ | ✓ | ✓ | ✓ | On Premise | Key–Value Store | |||||
Hawk Model Indexer | ✓ | ✓ | ✓ | On Premise | Graph | ||||||
Neo4EMF | ✓ | ✓ | ✓ | On Premise | Graph | ||||||
GreyCat | ✓ | ✓ | ✓ | ✓ | ✓ | On Premise | Versioned Graph | ||||
ChronoSphere
| ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
On Premise
|
Versioned Graph
|
GenMyModel | ✓ | ✓ | Cloud (SaaS) | SQL | |||||||
MDEForge | ✓ | ✓ | ✓ | ✓ | Cloud (SaaS) | Documents |
9.4 ChronoSphere as a generic model repository
10 Outlook and future work
ocl(String)
step in the query where the OCL statement is provided as a string. This string will then be analyzed and transformed into a graph query, which is then used as a subquery in the overall evaluation. By similar means, we intend to integrate the Epsilon Object Language [42] as sub-expressions in our query framework. This will allow ChronoSphere to offer support for several different query languages within the same underlying engine.