| 1 |
;;; BKNR Datastore |
|---|
| 2 |
|
|---|
| 3 |
;;;# Introduction |
|---|
| 4 |
;;; |
|---|
| 5 |
;;;## The prevalence model |
|---|
| 6 |
;;; |
|---|
| 7 |
;;; The BKNR datastore is a persistence solution for Lisp data. It |
|---|
| 8 |
;;; uses the prevalence model, which is based on the following |
|---|
| 9 |
;;; assumptions: |
|---|
| 10 |
;;; |
|---|
| 11 |
;;; All data is held in RAM. |
|---|
| 12 |
;;; |
|---|
| 13 |
;;; Data can be saved to disk at once into a snapshot file and is read |
|---|
| 14 |
;;; from that file at startup time. |
|---|
| 15 |
;;; |
|---|
| 16 |
;;; Changes to persistent data are written to a transaction log file |
|---|
| 17 |
;;; immediately, which can be replayed to restore all changes that |
|---|
| 18 |
;;; occured since the last snapshot was saved. |
|---|
| 19 |
;;; |
|---|
| 20 |
;;; Every kind of operation that needs to be logged is called a |
|---|
| 21 |
;;; "transaction", and such transactions are made explicit in the |
|---|
| 22 |
;;; program code. This is different from object-oriented databases, |
|---|
| 23 |
;;; where the fundamental transactions are object creation, object |
|---|
| 24 |
;;; deletion and slot access, which are not special cases in the |
|---|
| 25 |
;;; prevalence model at all. |
|---|
| 26 |
;;; |
|---|
| 27 |
;;; Isolation of transactions is achieved using thread locks. In the |
|---|
| 28 |
;;; simplest model used by the `mp-store', transactions are serialized |
|---|
| 29 |
;;; using a global lock. |
|---|
| 30 |
;;; |
|---|
| 31 |
;;; The transaction system is responsible for providing replay of |
|---|
| 32 |
;;; committed transactions after a server crash, but not for rollback |
|---|
| 33 |
;;; of failed transactions in a running server, except that failing |
|---|
| 34 |
;;; transactions are simply not logged onto disk. To roll back |
|---|
| 35 |
;;; transactions at points where exceptions might be excepted, use |
|---|
| 36 |
;;; ordinary Lisp programming techniques involving `unwind-protect' |
|---|
| 37 |
;;; and similar. |
|---|
| 38 |
;;; |
|---|
| 39 |
;;;## BKNR Datastore Design |
|---|
| 40 |
;;; |
|---|
| 41 |
;;; The design of the datastore aims to make explicit the |
|---|
| 42 |
;;; orthogonality of object system access (unlogged) and logging of |
|---|
| 43 |
;;; transactions (essentially independent of the object system). The |
|---|
| 44 |
;;; interface between transaction system and object system is |
|---|
| 45 |
;;; documented and allows for the implementation of alternative object |
|---|
| 46 |
;;; systems. For example, the blob subsystem is using the same |
|---|
| 47 |
;;; interface as the object subsystem. |
|---|
| 48 |
;;; |
|---|
| 49 |
;;; Previous versions of the BKNR Datastore allowed the creation of |
|---|
| 50 |
;;; multiple datastores in a single LISP process. However, this |
|---|
| 51 |
;;; feature was seldom used, and could be very confusing while |
|---|
| 52 |
;;; developing applications. The new version of the BKNR Datastore |
|---|
| 53 |
;;; supports only a single datastore, which is referenced by the |
|---|
| 54 |
;;; special variable `*STORE*'. |
|---|
| 55 |
;;; |
|---|
| 56 |
;;;## BKNR Object Datastore |
|---|
| 57 |
;;; |
|---|
| 58 |
;;; In addition to the transaction layer (in the file `txn.lisp'), the |
|---|
| 59 |
;;; BKNR datastore provides persistent CLOS objects using the |
|---|
| 60 |
;;; Metaobject Protocol. It provides a metaclass with which slots can |
|---|
| 61 |
;;; be defined as persistent (stored on snapshot) or transient. The |
|---|
| 62 |
;;; metaclass also prohibits slot accesses outside transactions, |
|---|
| 63 |
;;; provides unique IDs for all objects, and provides standard query |
|---|
| 64 |
;;; functions like `STORE-OBJECTS-WITH-CLASS' and |
|---|
| 65 |
;;; `STORE-OBJECTS-OF-CLASS'. The object datastore can be seamlessly |
|---|
| 66 |
;;; combined with BKNR indices and XML import/export. |
|---|
| 67 |
|
|---|
| 68 |
;;;# Obtaining and loading BKNR Datastore |
|---|
| 69 |
;;; |
|---|
| 70 |
;;; You can obtain the current CVS sources of BKNR by following the |
|---|
| 71 |
;;; instructions at `http://bknr.net/blog/bknr-devel'. Add the `experimental' |
|---|
| 72 |
;;; directory of BKNR to your `asdf:*central-registry*', and load the |
|---|
| 73 |
;;; indices module by evaluating the following form: |
|---|
| 74 |
|
|---|
| 75 |
(asdf:oos 'asdf:load-op :bknr.datastore) |
|---|
| 76 |
|
|---|
| 77 |
;;; Then switch to the `bknr.datastore' package to try out the tutorial. |
|---|
| 78 |
|
|---|
| 79 |
(in-package :bknr.datastore) |
|---|
| 80 |
|
|---|
| 81 |
;;;# A transaction system example |
|---|
| 82 |
|
|---|
| 83 |
;;; The first datastore we will build is very simple. We have a |
|---|
| 84 |
;;; counter variable for the store, and this counter variable can be |
|---|
| 85 |
;;; decremented and indecremented. We want this variable to be |
|---|
| 86 |
;;; persistent, so decrementing and incrementing it has to be done |
|---|
| 87 |
;;; through transactions that will be logged by the datastore. We also |
|---|
| 88 |
;;; define a `:BEFORE' method for the generic function `RESTORE-STORE' |
|---|
| 89 |
;;; to set the counter to `0' initially. This method will be called |
|---|
| 90 |
;;; every time the store is created or restored from disk. |
|---|
| 91 |
|
|---|
| 92 |
(defclass tutorial-store (mp-store) |
|---|
| 93 |
((counter :initform 0 :accessor tutorial-store-counter))) |
|---|
| 94 |
|
|---|
| 95 |
(defmethod restore-store :before ((store tutorial-store) &key until) |
|---|
| 96 |
(declare (ignore until)) |
|---|
| 97 |
(setf (tutorial-store-counter store) 0)) |
|---|
| 98 |
|
|---|
| 99 |
;;; The two transactions are declared like normal functions, but using |
|---|
| 100 |
;;; the `DEFTRANSACTION' macro. |
|---|
| 101 |
|
|---|
| 102 |
(deftransaction incf-counter () |
|---|
| 103 |
(incf (tutorial-store-counter *store*))) |
|---|
| 104 |
|
|---|
| 105 |
(deftransaction decf-counter () |
|---|
| 106 |
(decf (tutorial-store-counter *store*))) |
|---|
| 107 |
|
|---|
| 108 |
;;; When looking at the macro-expanded form of `DEFTRANSACTION', we |
|---|
| 109 |
;;; see that `DEFTRANSACTION' defines two functions, a toplevel |
|---|
| 110 |
;;; function that creates a transaction object and calls the method |
|---|
| 111 |
;;; `EXECUTE' on it, and a function that contains the actual |
|---|
| 112 |
;;; transaction code and that will be called in the context of the |
|---|
| 113 |
;;; transaction, and logged to disk. |
|---|
| 114 |
|
|---|
| 115 |
(PROGN (DEFUN TX-DECF-COUNTER () |
|---|
| 116 |
(UNLESS (IN-TRANSACTION-P) (ERROR 'NOT-IN-TRANSACTION)) |
|---|
| 117 |
(DECF (TUTORIAL-STORE-COUNTER *STORE*))) |
|---|
| 118 |
(DEFUN DECF-COUNTER () |
|---|
| 119 |
(EXECUTE (MAKE-INSTANCE 'TRANSACTION |
|---|
| 120 |
:FUNCTION-SYMBOL 'TX-DECF-COUNTER |
|---|
| 121 |
:TIMESTAMP (GET-UNIVERSAL-TIME) |
|---|
| 122 |
:ARGS (LIST))))) |
|---|
| 123 |
|
|---|
| 124 |
;;; The new datastore only supports a single datastore instance per |
|---|
| 125 |
;;; LISP instance. When creating a `STORE' object, the `*STORE*' |
|---|
| 126 |
;;; special variable is modified to point to the datastore. Thus, we |
|---|
| 127 |
;;; can create our simple datastore by creating an object of type |
|---|
| 128 |
;;; `TUTORIAL-STORE'. The transaction log will be store in the |
|---|
| 129 |
;;; directory "/tmp/tutorial-store". |
|---|
| 130 |
|
|---|
| 131 |
(close-store) |
|---|
| 132 |
(make-instance 'tutorial-store :directory "/tmp/tutorial-store/" |
|---|
| 133 |
:subsystems nil) |
|---|
| 134 |
; Warning: restoring #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 135 |
; => #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 136 |
|
|---|
| 137 |
(tutorial-store-counter *store*) |
|---|
| 138 |
; => 0 |
|---|
| 139 |
(incf-counter) |
|---|
| 140 |
; => 1 |
|---|
| 141 |
(incf-counter) |
|---|
| 142 |
; => 2 |
|---|
| 143 |
(decf-counter) |
|---|
| 144 |
; => 1 |
|---|
| 145 |
|
|---|
| 146 |
;;; The three transactions have been logged to the transaction log in |
|---|
| 147 |
;;; "/tmp/tutorial-store/", as we can see: |
|---|
| 148 |
|
|---|
| 149 |
(with-open-file (s "/tmp/tutorial-store/current/transaction-log" |
|---|
| 150 |
:direction :input) |
|---|
| 151 |
(file-length s)) |
|---|
| 152 |
; => 126 |
|---|
| 153 |
(incf-counter) |
|---|
| 154 |
; => 2 |
|---|
| 155 |
(with-open-file (s "/tmp/tutorial-store/current/transaction-log" |
|---|
| 156 |
:direction :input) |
|---|
| 157 |
(file-length s)) |
|---|
| 158 |
; => 168 |
|---|
| 159 |
|
|---|
| 160 |
;;; The transaction log is kept in a directory called "current", which |
|---|
| 161 |
;;; is where the currently active version of the snapshots and log |
|---|
| 162 |
;;; files are kept. When a datastore is snapshotted, the "current" |
|---|
| 163 |
;;; directory is backupped to another directory with the current date, |
|---|
| 164 |
;;; and snapshots are created in the new "current" directory. However, |
|---|
| 165 |
;;; we cannot snapshot our tutorial datastore, as we cannot snapshot |
|---|
| 166 |
;;; the persistent data (the counter value). |
|---|
| 167 |
|
|---|
| 168 |
(snapshot) |
|---|
| 169 |
; => Error in function (METHOD SNAPSHOT-STORE NIL (STORE)): |
|---|
| 170 |
; => Cannot snapshot store without subsystems... |
|---|
| 171 |
; => [Condition of type SIMPLE-ERROR] |
|---|
| 172 |
|
|---|
| 173 |
;;; We can close the store by using the function `CLOSE-STORE'. |
|---|
| 174 |
*store* |
|---|
| 175 |
; => #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 176 |
(close-store) |
|---|
| 177 |
; => NIL |
|---|
| 178 |
(boundp '*store*) |
|---|
| 179 |
; => NIL |
|---|
| 180 |
|
|---|
| 181 |
;;; The store can then be recreated, and the transaction log will be |
|---|
| 182 |
;;; read and executed upon restore. |
|---|
| 183 |
(make-instance 'tutorial-store :directory "/tmp/tutorial-store/" |
|---|
| 184 |
:subsystems nil) |
|---|
| 185 |
|
|---|
| 186 |
; Warning: restoring #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 187 |
; Warning: loading transaction log |
|---|
| 188 |
; /tmp/tutorial-store/current/transaction-log |
|---|
| 189 |
; => #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 190 |
(tutorial-store-counter *store*) |
|---|
| 191 |
; => 2 |
|---|
| 192 |
|
|---|
| 193 |
;;; The store can also be restored in a later LISP session. Make sure |
|---|
| 194 |
;;; that all the code necessary to the execution of the transaction |
|---|
| 195 |
;;; log has been loaded before restoring the datastore. A later |
|---|
| 196 |
;;; version of the datastore will log all the code necessary in the |
|---|
| 197 |
;;; datastore itself, so that code and data are synchronized. |
|---|
| 198 |
|
|---|
| 199 |
;;;## Debugging the datastore |
|---|
| 200 |
;;; |
|---|
| 201 |
;;; By setting the `*STORE-DEBUG*' special variable to `T', the |
|---|
| 202 |
;;; datastore prints a lot of useful warnings. For example |
|---|
| 203 |
|
|---|
| 204 |
;;; You can also restore to a certain point in time, by specifying the |
|---|
| 205 |
;;; `UNTIL' argument of `RESTORE-STORE'. |
|---|
| 206 |
|
|---|
| 207 |
(setf *store-debug* t) |
|---|
| 208 |
; => T |
|---|
| 209 |
(restore-store *store*) |
|---|
| 210 |
; restoring #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 211 |
; loading transaction log /tmp/tutorial-store/current/transaction-log |
|---|
| 212 |
; executing transaction #<TRANSACTION 21.04.2008 07:08:22 TX-INCF-COUNTER > at timestamp 3417743302 |
|---|
| 213 |
; executing transaction #<TRANSACTION 21.04.2008 07:08:25 TX-INCF-COUNTER > at timestamp 3417743305 |
|---|
| 214 |
; executing transaction #<TRANSACTION 21.04.2008 07:08:26 TX-DECF-COUNTER > at timestamp 3417743306 |
|---|
| 215 |
; executing transaction #<TRANSACTION 21.04.2008 07:08:34 TX-INCF-COUNTER > at timestamp 3417743314 |
|---|
| 216 |
; => NIL |
|---|
| 217 |
(tutorial-store-counter *store*) |
|---|
| 218 |
; => 2 |
|---|
| 219 |
; !! Update the timestamp below to correspond to the fist transaction executed above !! |
|---|
| 220 |
(restore-store *store* :until 3417743302) ... |
|---|
| 221 |
; restoring #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 222 |
; loading transaction log /tmp/tutorial-store/current/transaction-log |
|---|
| 223 |
; executing transaction #<TRANSACTION 21.04.2008 07:08:22 TX-INCF-COUNTER > at timestamp 3417743302 |
|---|
| 224 |
; creating log file backup: /tmp/tutorial-store/current/transaction-log.backup |
|---|
| 225 |
; truncating transaction log at position 42. |
|---|
| 226 |
(tutorial-store-counter *store*) |
|---|
| 227 |
; => 1 |
|---|
| 228 |
|
|---|
| 229 |
;;;## Adding a subsystem |
|---|
| 230 |
|
|---|
| 231 |
;;; Now that we can restore the counter state by loading the |
|---|
| 232 |
;;; transaction log, we want to add a subsystem to be able to snapshot |
|---|
| 233 |
;;; the state of the counter. Thus, we won't need to execute every |
|---|
| 234 |
;;; single incrementing or decrementing transaction to restore our |
|---|
| 235 |
;;; persistent state. |
|---|
| 236 |
|
|---|
| 237 |
;;; To do this, we have to create a store-subsystem that will be able |
|---|
| 238 |
;;; to write the counter number to a file and to reload it on restore. |
|---|
| 239 |
|
|---|
| 240 |
(defclass counter-subsystem () |
|---|
| 241 |
()) |
|---|
| 242 |
|
|---|
| 243 |
;;; Three methods are used to interact with the subsystem. |
|---|
| 244 |
;;; The first method is `INITIALIZE-SUBSYSTEM', which is called after |
|---|
| 245 |
;;; the store has been created and restored. It is used to initialize |
|---|
| 246 |
;;; certain parameters of the subsystem. We won't use this method |
|---|
| 247 |
;;; here, as our subsystem is very simple. |
|---|
| 248 |
;;; The second method is `SNAPSHOT-SUBSYSTEM', which is called when |
|---|
| 249 |
;;; the store is snapshotted. The subsystem has to store the |
|---|
| 250 |
;;; persistent data it handles to a snapshot file inside the current |
|---|
| 251 |
;;; directory of the store. Our `COUNTER-SUBSYSTEM' writes the current |
|---|
| 252 |
;;; value of the counter to a file named "counter" in the current |
|---|
| 253 |
;;; directory of the store (the old directory has been renamed). |
|---|
| 254 |
|
|---|
| 255 |
(defmethod snapshot-subsystem ((store tutorial-store) |
|---|
| 256 |
(subsystem counter-subsystem)) |
|---|
| 257 |
(let* ((store-dir (ensure-store-current-directory store)) |
|---|
| 258 |
(counter-pathname |
|---|
| 259 |
(make-pathname :name "counter" :defaults store-dir))) |
|---|
| 260 |
(with-open-file (s counter-pathname :direction :output) |
|---|
| 261 |
(write (tutorial-store-counter store) :stream s)))) |
|---|
| 262 |
|
|---|
| 263 |
;;; Finally, the method `RESTORE-SUBSYSTEM' is called at restore time |
|---|
| 264 |
;;; to tell the subsystem to read back its persistent state from the |
|---|
| 265 |
;;; current directory of the store. Our `COUNTER-SUBSYSTEM' reads back |
|---|
| 266 |
;;; the counter value from the file named "counter". If it can't find |
|---|
| 267 |
;;; the file (for example if this is the first time that our datastore |
|---|
| 268 |
;;; is created, the file won't be there, so we issue a warning and set |
|---|
| 269 |
;;; the counter value to 0. |
|---|
| 270 |
|
|---|
| 271 |
(defmethod restore-subsystem ((store tutorial-store) |
|---|
| 272 |
(subsystem counter-subsystem) &key |
|---|
| 273 |
until) |
|---|
| 274 |
(declare (ignore until)) |
|---|
| 275 |
(let* ((store-dir (ensure-store-current-directory store)) |
|---|
| 276 |
(counter-pathname |
|---|
| 277 |
(make-pathname :name "counter" :defaults store-dir))) |
|---|
| 278 |
(if (probe-file counter-pathname) |
|---|
| 279 |
(with-open-file (s counter-pathname :direction :input) |
|---|
| 280 |
(let ((counter (read s))) |
|---|
| 281 |
(setf (tutorial-store-counter store) counter))) |
|---|
| 282 |
(progn |
|---|
| 283 |
(warn "Could not find store counter value, setting to 0.") |
|---|
| 284 |
(setf (tutorial-store-counter store) 0))))) |
|---|
| 285 |
|
|---|
| 286 |
;;; Now we can close our current store, and instantiate it anew with a |
|---|
| 287 |
;;; `COUNTER-SUBSYSTEM'. |
|---|
| 288 |
|
|---|
| 289 |
(close-store) |
|---|
| 290 |
; => NIL |
|---|
| 291 |
(make-instance 'tutorial-store :directory "/tmp/tutorial-store/" |
|---|
| 292 |
:subsystems (list (make-instance 'counter-subsystem))) |
|---|
| 293 |
; Warning: restoring #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 294 |
; Warning: Could not find store counter value, setting to 0. |
|---|
| 295 |
; Warning: loading transaction log |
|---|
| 296 |
; /tmp/tutorial-store/current/transaction-log |
|---|
| 297 |
; => #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 298 |
(snapshot) |
|---|
| 299 |
; Snapshotting subsystem #<COUNTER-SUBSYSTEM #xE65F866> of #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 300 |
; Successfully snapshotted #<COUNTER-SUBSYSTEM #xE65F866> of #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 301 |
; => NIL |
|---|
| 302 |
(restore) |
|---|
| 303 |
; restoring #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 304 |
; Restoring the subsystem #<COUNTER-SUBSYSTEM #xE65F866> of #<TUTORIAL-STORE DIR: "/tmp/tutorial-store/"> |
|---|
| 305 |
; => NUL |
|---|
| 306 |
|
|---|
| 307 |
;;;# An object store example |
|---|
| 308 |
|
|---|
| 309 |
;;; The BKNR object datastore is implemented using a special subsystem |
|---|
| 310 |
;;; `STORE-OBJECT-SUBSYSTEM'. Every object referenced by the store |
|---|
| 311 |
;;; object subsystem has a unique ID, and must be of the class |
|---|
| 312 |
;;; `STORE-OBJECT'. The ID counter in the store-object subsystem is |
|---|
| 313 |
;;; incremented on every object creation. |
|---|
| 314 |
;;; |
|---|
| 315 |
;;; All store objects have to be of the metaclass `PERSISTENT-CLASS', |
|---|
| 316 |
;;; which will ensure the object is referenced in the base indices of |
|---|
| 317 |
;;; the object datastore, and that slot access is only done inside a |
|---|
| 318 |
;;; transaction. The subsystem makes heavy use of BKNR indices, and |
|---|
| 319 |
;;; indexes object by ID and by class. |
|---|
| 320 |
|
|---|
| 321 |
;;; The ID index can be queried using the functions |
|---|
| 322 |
;;; `STORE-OBJECT-WITH-ID', which returns the object with the |
|---|
| 323 |
;;; requested ID, `ALL-STORE-OBJECTS' which returns all current store |
|---|
| 324 |
;;; objects, and `MAP-STORE-OBJECTS', which applies a function |
|---|
| 325 |
;;; iteratively to each store object. The class index can be queried |
|---|
| 326 |
;;; using the functions `ALL-STORE-CLASSES', which returns the names |
|---|
| 327 |
;;; of all the classes currently present in the datastore, and |
|---|
| 328 |
;;; `STORE-OBJECTS-WITH-CLASS', which returns all the objects of a |
|---|
| 329 |
;;; specific class (across superclasses also, so |
|---|
| 330 |
;;; `(STORE-OBJECTS-WITH-CLASS \'STORE-OBJECT)' returns all the |
|---|
| 331 |
;;; existing store objects. |
|---|
| 332 |
|
|---|
| 333 |
;;;## Store and object creation |
|---|
| 334 |
|
|---|
| 335 |
;;; We can create an object datastore by creating a `STORE' with the |
|---|
| 336 |
;;; subsystem `STORE-OBJECT-SUBSYSTEM'. |
|---|
| 337 |
|
|---|
| 338 |
(close-store) |
|---|
| 339 |
(make-instance 'mp-store :directory "/tmp/object-store/" |
|---|
| 340 |
:subsystems (list |
|---|
| 341 |
(make-instance 'store-object-subsystem))) |
|---|
| 342 |
|
|---|
| 343 |
; Warning: restoring #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 344 |
; restoring #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 345 |
; Restoring the subsystem #<STORE-OBJECT-SUBSYSTEM #xE63F866> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 346 |
(all-store-objects) |
|---|
| 347 |
; => NIL |
|---|
| 348 |
|
|---|
| 349 |
;;; We can now create a few store objects (which is not very |
|---|
| 350 |
;;; interesting in itself). Store objects have to be created inside a |
|---|
| 351 |
;;; transaction so that the object creation is logged into the |
|---|
| 352 |
;;; transaction log. This is done by using the transaction |
|---|
| 353 |
;;; `MAKE-OBJECT'. The transaction also automatically gets a unique ID |
|---|
| 354 |
;;; from the store object subsystem. |
|---|
| 355 |
|
|---|
| 356 |
(make-object 'store-object) |
|---|
| 357 |
; => #<STORE-OBJECT ID: 0> |
|---|
| 358 |
(make-object 'store-object) |
|---|
| 359 |
; => #<STORE-OBJECT ID: 1> |
|---|
| 360 |
(all-store-objects) |
|---|
| 361 |
; => (#<STORE-OBJECT ID: 0> #<STORE-OBJECT ID: 1>) |
|---|
| 362 |
(all-store-classes) |
|---|
| 363 |
; => (STORE-OBJECT) |
|---|
| 364 |
|
|---|
| 365 |
;;; Object deletion also has to be done through the transaction |
|---|
| 366 |
;;; `DELETE-OBJECT', which will log the deletion of the object in the |
|---|
| 367 |
;;; transaction log, and remove the object from all its indices. |
|---|
| 368 |
|
|---|
| 369 |
(make-object 'store-object) |
|---|
| 370 |
; executing transaction #<TRANSACTION 21.04.2008 08:02:10 TX-MAKE-OBJECT STORE-OBJECT ID 2> at timestamp 3417746530 |
|---|
| 371 |
; => #<STORE-OBJECT ID: 12> |
|---|
| 372 |
(store-object-with-id 2) |
|---|
| 373 |
; => #<STORE-OBJECT ID: 2> |
|---|
| 374 |
(delete-object (store-object-with-id 2)) |
|---|
| 375 |
; executing transaction #<TRANSACTION 21.04.2008 08:52:14 TX-DELETE-OBJECT 2> at timestamp 3417749534 |
|---|
| 376 |
; => T |
|---|
| 377 |
(store-object-with-id 2) |
|---|
| 378 |
; => NIL |
|---|
| 379 |
|
|---|
| 380 |
;;;## Defining persistent classes |
|---|
| 381 |
|
|---|
| 382 |
;;; A more interesting thing is to create our own persistent class, |
|---|
| 383 |
;;; which we will call `TUTORIAL-OBJECT'. |
|---|
| 384 |
|
|---|
| 385 |
(defclass tutorial-object (store-object) |
|---|
| 386 |
((a :initarg :a :reader tutorial-object-a)) |
|---|
| 387 |
(:metaclass persistent-class)) |
|---|
| 388 |
|
|---|
| 389 |
;;; We can also use the `DEFINE-PERSISTENT-CLASS' to define the class |
|---|
| 390 |
;;; `TUTORIAL-OBJECT': |
|---|
| 391 |
|
|---|
| 392 |
(define-persistent-class tutorial-object () |
|---|
| 393 |
((a :read))) |
|---|
| 394 |
|
|---|
| 395 |
;;; This gets macroexpanded to the following form. |
|---|
| 396 |
|
|---|
| 397 |
(DEFINE-BKNR-CLASS TUTORIAL-OBJECT |
|---|
| 398 |
(STORE-OBJECT) |
|---|
| 399 |
((A :READ)) |
|---|
| 400 |
(:METACLASS PERSISTENT-CLASS)) |
|---|
| 401 |
|
|---|
| 402 |
;;; The macro DEFINE-BKNR-CLASS is a short form of the macro DEFCLASS, |
|---|
| 403 |
;;; it expands to the following code. The `EVAL-WHEN' is |
|---|
| 404 |
;;; there to ensure timely definition of the accessor methods. |
|---|
| 405 |
|
|---|
| 406 |
(EVAL-WHEN (:COMPILE-TOPLEVEL :LOAD-TOPLEVEL :EXECUTE) |
|---|
| 407 |
(DEFCLASS TUTORIAL-OBJECT |
|---|
| 408 |
(STORE-OBJECT) |
|---|
| 409 |
((A :READER TUTORIAL-OBJECT-A :INITARG :A)) |
|---|
| 410 |
(:METACLASS PERSISTENT-CLASS))) |
|---|
| 411 |
|
|---|
| 412 |
;;; We can now create a few instance of `TUTORIAL-OBJECT': |
|---|
| 413 |
|
|---|
| 414 |
(make-object 'tutorial-object :a 2) |
|---|
| 415 |
; => #<TUTORIAL-OBJECT ID: 3> |
|---|
| 416 |
(make-object 'tutorial-object :a 2) |
|---|
| 417 |
; => #<TUTORIAL-OBJECT ID: 4> |
|---|
| 418 |
(make-object 'tutorial-object :a 2) |
|---|
| 419 |
; => #<TUTORIAL-OBJECT ID: 5> |
|---|
| 420 |
|
|---|
| 421 |
(store-object-with-id 5) |
|---|
| 422 |
; => #<TUTORIAL-OBJECT ID: 5> |
|---|
| 423 |
|
|---|
| 424 |
(all-store-classes) |
|---|
| 425 |
; => (STORE-OBJECT TUTORIAL-OBJECT) |
|---|
| 426 |
|
|---|
| 427 |
(store-objects-with-class 'tutorial-object) |
|---|
| 428 |
; => (#<TUTORIAL-OBJECT ID: 3> #<TUTORIAL-OBJECT ID: 4> |
|---|
| 429 |
; #<TUTORIAL-OBJECT ID: 5>) |
|---|
| 430 |
(store-objects-with-class 'store-object) |
|---|
| 431 |
; => (#<STORE-OBJECT ID: 0> #<STORE-OBJECT ID: 1> |
|---|
| 432 |
; #<FOO ID: 2> #<TUTORIAL-OBJECT ID: 3> |
|---|
| 433 |
; #<TUTORIAL-OBJECT ID: 4> #<TUTORIAL-OBJECT ID: 5>) |
|---|
| 434 |
|
|---|
| 435 |
;;; In order to change the slot values of persistent object, the |
|---|
| 436 |
;;; application needs to be in a transaction context. This can be |
|---|
| 437 |
;;; done either by invoking a named transaction as above, or by |
|---|
| 438 |
;;; creating an anonymous transaction. In an anonymous transaction, |
|---|
| 439 |
;;; all write accesses to persistent objects are logged. |
|---|
| 440 |
|
|---|
| 441 |
(define-persistent-class tutorial-object2 () |
|---|
| 442 |
((b :update))) |
|---|
| 443 |
|
|---|
| 444 |
(make-object 'tutorial-object2 :b 3) |
|---|
| 445 |
; executing transaction #<TRANSACTION 21.04.2008 08:03:27 TX-MAKE-OBJECT TUTORIAL-OBJECT2 ID 6 B 3> at timestamp 3417746607 |
|---|
| 446 |
; => #<TUTORIAL-OBJECT2 ID: 6> |
|---|
| 447 |
(setf (slot-value (store-object-with-id 6) 'b) 4) |
|---|
| 448 |
; => Error |
|---|
| 449 |
; Attempt to set persistent slot B of #<TUTORIAL-OBJECT2 ID: 6> outside of a transaction |
|---|
| 450 |
(with-transaction () |
|---|
| 451 |
(setf (slot-value (store-object-with-id 6) 'b) 4)) |
|---|
| 452 |
; => 4 |
|---|
| 453 |
(tutorial-object2-b (store-object-with-id 6)) |
|---|
| 454 |
; => 4 |
|---|
| 455 |
|
|---|
| 456 |
;;;## Object creation and deletion protocol |
|---|
| 457 |
|
|---|
| 458 |
;;; Persistent objects have the metaclass `PERSISTENT-CLASS', and have |
|---|
| 459 |
;;; to be created using the function `MAKE-OBJECT'. This creates an |
|---|
| 460 |
;;; instance of the object inside a transaction, sets its ID slot |
|---|
| 461 |
;;; appropriately, and then calls `INITIALIZE-PERSISTENT-INSTANCE' and |
|---|
| 462 |
;;; `INITIALIZE-TRANSIENT-INSTANCE'. The first method is called when |
|---|
| 463 |
;;; the object is created inside a transaction, but not if the object |
|---|
| 464 |
;;; is being restored from the snapshot file. This method has to be |
|---|
| 465 |
;;; overridden in order to initialize persistent |
|---|
| 466 |
;;; slots. `INITIALIZE-TRANSIENT-INSTANCE' is called at object |
|---|
| 467 |
;;; creation inside a transaction and at object creation during |
|---|
| 468 |
;;; restore. It is used to initialize the transient slots (not logged |
|---|
| 469 |
;;; to the snapshot file) of a persistent object. |
|---|
| 470 |
;;; |
|---|
| 471 |
;;; We can define the following class with a transient and a |
|---|
| 472 |
;;; persistent slot. |
|---|
| 473 |
|
|---|
| 474 |
(define-persistent-class protocol-object () |
|---|
| 475 |
((a :update :transient t) |
|---|
| 476 |
(b :update))) |
|---|
| 477 |
|
|---|
| 478 |
;;; We can modify the slot `A' outside a transaction: |
|---|
| 479 |
(make-object 'protocol-object :a 1 :b 2) |
|---|
| 480 |
; executing transaction #<TRANSACTION 21.04.2008 08:10:49 TX-MAKE-OBJECT PROTOCOL-OBJECT ID 7 A 1 B 2> at timestamp 3417747049 |
|---|
| 481 |
; => #<PROTOCOL-OBJECT ID: 7> |
|---|
| 482 |
(setf (protocol-object-a (store-object-with-id 7)) 2) |
|---|
| 483 |
; => 2 |
|---|
| 484 |
|
|---|
| 485 |
;;; However, we cannot modify the slot `B', as it is persistent and |
|---|
| 486 |
;;; has to be changed inside a transaction. |
|---|
| 487 |
|
|---|
| 488 |
(setf (protocol-object-b (store-object-with-id 7)) 4) |
|---|
| 489 |
; => Error |
|---|
| 490 |
; Attempt to set persistent slot B of #<PROTOCOL-OBJECT ID: 7> outside of a transaction |
|---|
| 491 |
|
|---|
| 492 |
;;; An object can be removed from the datastore using the transaction |
|---|
| 493 |
;;; `DELETE-OBJECT', which calls the method `DESTROY-OBJECT' on the |
|---|
| 494 |
;;; object. Special actions at deletion time have to be added by |
|---|
| 495 |
;;; overriding `DESTROY-OBJECT'. The basic action is to remove the |
|---|
| 496 |
;;; object from all its indices. |
|---|
| 497 |
|
|---|
| 498 |
;;;## Snapshotting an object datastore |
|---|
| 499 |
|
|---|
| 500 |
;;; We can snapshot the persistent state of all created objects by |
|---|
| 501 |
;;; using `SNAPSHOT'. |
|---|
| 502 |
(snapshot) |
|---|
| 503 |
; Snapshotting subsystem #<STORE-OBJECT-SUBSYSTEM #xE54991E> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 504 |
; Successfully snapshotted #<STORE-OBJECT-SUBSYSTEM #xE54991E> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 505 |
|
|---|
| 506 |
;;; This will create a backup directory containing the old transaction |
|---|
| 507 |
;;; log, and the creation of a snapshot file in the "current" |
|---|
| 508 |
;;; directory. |
|---|
| 509 |
|
|---|
| 510 |
(directory "/tmp/object-store/**/*.*") |
|---|
| 511 |
; => (#P"/tmp/object-store/20080421T061210/random-state" |
|---|
| 512 |
; #P"/tmp/object-store/20080421T061210/transaction-log" |
|---|
| 513 |
; #P"/tmp/object-store/current/random-state" |
|---|
| 514 |
; #P"/tmp/object-store/current/store-object-subsystem-snapshot" |
|---|
| 515 |
; #P"/tmp/object-store/current/transaction-log") |
|---|
| 516 |
|
|---|
| 517 |
;;; The snapshot file contains all persistent objects present at |
|---|
| 518 |
;;; snapshotting time, and the value of their persistent |
|---|
| 519 |
;;; slots. Further transaction are recorded in a new transaction log. |
|---|
| 520 |
|
|---|
| 521 |
;;;## Adding indices to store objects |
|---|
| 522 |
|
|---|
| 523 |
;;; The object datastore builds upon the functionality of the BKNR |
|---|
| 524 |
;;; indices system. All store objects are of the metaclass |
|---|
| 525 |
;;; `INDEXED-CLASS', so adding indices is seamless. Indices are |
|---|
| 526 |
;;; transient, and are rebuilt every time the datastore is |
|---|
| 527 |
;;; restored. Adding an index on a transient slot or on a persistent |
|---|
| 528 |
;;; slot makes no difference. |
|---|
| 529 |
|
|---|
| 530 |
(define-persistent-class gorilla () |
|---|
| 531 |
((name :read :index-type string-unique-index |
|---|
| 532 |
:index-reader gorilla-with-name |
|---|
| 533 |
:index-values all-gorillas) |
|---|
| 534 |
(mood :read :index-type hash-index |
|---|
| 535 |
:index-reader gorillas-with-mood |
|---|
| 536 |
:index-keys all-gorilla-moods))) |
|---|
| 537 |
|
|---|
| 538 |
(make-object 'gorilla :name "lucy" :mood :aggressive) |
|---|
| 539 |
; => #<GORILLA ID: 8> |
|---|
| 540 |
(make-object 'gorilla :name "john" :mood :playful) |
|---|
| 541 |
; => #<GORILLA ID: 9> |
|---|
| 542 |
(make-object 'gorilla :name "peter" :mood :playful) |
|---|
| 543 |
; => #<GORILLA ID: 10> |
|---|
| 544 |
(gorilla-with-name "lucy") |
|---|
| 545 |
; => #<GORILLA ID: 8> |
|---|
| 546 |
(gorillas-with-mood :playful) |
|---|
| 547 |
; => (#<GORILLA ID: 10> #<GORILLA ID: 9>) |
|---|
| 548 |
|
|---|
| 549 |
;;;## Adding blobs |
|---|
| 550 |
|
|---|
| 551 |
;;; A blob is a Binary Large OBject, that means it is a normal |
|---|
| 552 |
;;; persistent object with an associated binary data (that most of the |
|---|
| 553 |
;;; time is quite large). The object datastore supports storing this |
|---|
| 554 |
;;; large binary data outside the transaction log and the snapshot |
|---|
| 555 |
;;; file in order not to strain the store memory footprint too much, |
|---|
| 556 |
;;; and to be able to access the binary data from outside the LISP |
|---|
| 557 |
;;; session. This can be useful in order to copy the binary data using |
|---|
| 558 |
;;; the operating system calls directly. Blobs are used to store |
|---|
| 559 |
;;; images in the BKNR Web Framework (in fact, eboy.com contains more |
|---|
| 560 |
;;; than 40000 images). They have also been used to store MP3 files |
|---|
| 561 |
;;; for the GPN interactive DJ. |
|---|
| 562 |
;;; |
|---|
| 563 |
;;; In addition to the binary data, a blob object also holds a `TYPE' |
|---|
| 564 |
;;; and a `TIMESTAMP'. The type of a blob object is a keyword somehow |
|---|
| 565 |
;;; identifying the type of binary data it stores. For example, for |
|---|
| 566 |
;;; the images of the eboy datastore, we used the keywords `:JPEG', |
|---|
| 567 |
;;; `:PNG', `:GIF' to identity the different file formats used to |
|---|
| 568 |
;;; store images. The timestamp identifies the time of creation of the |
|---|
| 569 |
;;; blob object (this can be useful to cache binary data of blob |
|---|
| 570 |
;;; objects in a web server context). |
|---|
| 571 |
;;; |
|---|
| 572 |
;;; Stores are implemented in a custom subsystem, which takes as key |
|---|
| 573 |
;;; argument `:DIRECTORY' the name of a directory where the binary |
|---|
| 574 |
;;; data of the blob objects is stored as a simple file. This |
|---|
| 575 |
;;; directory can be further partitioned dynamically by the datastore, |
|---|
| 576 |
;;; when provided with the argument `:N-BLOBS-PER-DIRECTORY'. The |
|---|
| 577 |
;;; value of this argument is stored in the directory of the |
|---|
| 578 |
;;; datastore, so that a future instance of the blob subsystem is |
|---|
| 579 |
;;; initialised correctly. |
|---|
| 580 |
;;; |
|---|
| 581 |
;;; We can now add blob support to our existing object datastore by |
|---|
| 582 |
;;; adding the blob subsystem to its list of subsystems. |
|---|
| 583 |
|
|---|
| 584 |
(close-store) |
|---|
| 585 |
(make-instance 'mp-store :directory "/tmp/object-store/" |
|---|
| 586 |
:subsystems (list |
|---|
| 587 |
(make-instance 'store-object-subsystem) |
|---|
| 588 |
(make-instance 'blob-subsystem))) |
|---|
| 589 |
|
|---|
| 590 |
;;; The blob subsystem provides a few functions and transactions to |
|---|
| 591 |
;;; work with blobs. To show how to use these functions, we will |
|---|
| 592 |
;;; define a blob class in our example store. A photo is simply a |
|---|
| 593 |
;;; binary object with a name. |
|---|
| 594 |
|
|---|
| 595 |
(define-persistent-class photo (blob) |
|---|
| 596 |
((name :read))) |
|---|
| 597 |
|
|---|
| 598 |
;;; A blob can be created using the function `MAKE-BLOB-FROM-FILE', |
|---|
| 599 |
;;; which is a wrapper around `TX-MAKE-OBJECT' and the function |
|---|
| 600 |
;;; `BLOB-FROM-FILE'. The method `BLOB-FROM-FILE' fills the binary |
|---|
| 601 |
;;; data of a blob object by reading the content of a file. This |
|---|
| 602 |
;;; binary data is then stored in a file named after the ID of the |
|---|
| 603 |
;;; object in the blob root directory of the blob subsystem. |
|---|
| 604 |
|
|---|
| 605 |
(make-blob-from-file "/tmp/bla.jpg" 'photo :name "foobar" |
|---|
| 606 |
:type :jpg) |
|---|
| 607 |
; => #<PHOTO ID: 11, TYPE: jpg> |
|---|
| 608 |
|
|---|
| 609 |
;;; We can work with the photo object in the same way as when we work |
|---|
| 610 |
;;; with a normal object. However, we can access the binary data using |
|---|
| 611 |
;;; the methods `BLOB-PATHNAME', which returns the pathname to the |
|---|
| 612 |
;;; file in the blob root that holds the binary data of the |
|---|
| 613 |
;;; object. |
|---|
| 614 |
|
|---|
| 615 |
(blob-pathname (store-object-with-id 11)) |
|---|
| 616 |
; => #P"/tmp/object-store/blob-root/11" |
|---|
| 617 |
|
|---|
| 618 |
;;; The method `BLOB-TO-FILE' and `BLOB-TO-STREAM' write the binary |
|---|
| 619 |
;;; data of the object to the specified file or stream (the stream has |
|---|
| 620 |
;;; to be of the type `(UNSIGNED-BYTE 8)'). The macro `WITH-OPEN-BLOB' |
|---|
| 621 |
;;; is provided as wrapper around the `WITH-OPEN-FILE' macro. |
|---|
| 622 |
|
|---|
| 623 |
;;;## Relaxed references |
|---|
| 624 |
|
|---|
| 625 |
;;; It sometimes happens that a persistent object is deleted while it |
|---|
| 626 |
;;; still is referenced by another object. This can lead to problems |
|---|
| 627 |
;;; when snapshotting and restoring the datastore, as the referenced |
|---|
| 628 |
;;; object is not available anymore. |
|---|
| 629 |
;;; |
|---|
| 630 |
;;; When a slot is specified as being a relaxed object reference slot |
|---|
| 631 |
;;; using the slot option `:RELAXED-OBJECT-REFERENCE', a reference to |
|---|
| 632 |
;;; an unexistent object can be encoded during snapshot. The object |
|---|
| 633 |
;;; subsystem issues a warning when a reference to a non-existent |
|---|
| 634 |
;;; object is encoded. When a reference to a deleted object is decoded |
|---|
| 635 |
;;; form the snapshot file, a `NIL' value is returned if the slot from |
|---|
| 636 |
;;; where the object is referenced supports relaxed references. Else, |
|---|
| 637 |
;;; an error is thrown. |
|---|
| 638 |
|
|---|
| 639 |
(define-persistent-class relaxed-object () |
|---|
| 640 |
((a :update :relaxed-object-reference t))) |
|---|
| 641 |
|
|---|
| 642 |
(make-object 'relaxed-object) |
|---|
| 643 |
; => #<RELAXED-OBJECT ID: 12> |
|---|
| 644 |
(make-object 'relaxed-object) |
|---|
| 645 |
; => #<RELAXED-OBJECT ID: 13> |
|---|
| 646 |
(with-transaction () |
|---|
| 647 |
(setf (slot-value (store-object-with-id 12) 'a) (store-object-with-id 13))) |
|---|
| 648 |
; => #<RELAXED-OBJECT ID: 13> |
|---|
| 649 |
(delete-object (store-object-with-id 13)) |
|---|
| 650 |
; => T |
|---|
| 651 |
(snapshot) |
|---|
| 652 |
; Warning: Backup of the datastore in /tmp/object-store/20080421T064811/. |
|---|
| 653 |
; While executing: (:INTERNAL (SNAPSHOT-STORE (STORE))), in process worker(1750). |
|---|
| 654 |
; Snapshotting subsystem #<STORE-OBJECT-SUBSYSTEM #xE6069EE> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 655 |
; Warning: Encoding reference to destroyed object with ID 13 from slot A of object RELAXED-OBJECT with ID 12. |
|---|
| 656 |
; While executing: #<STANDARD-METHOD ENCODE-OBJECT (STORE-OBJECT T)>, in process worker(1750). |
|---|
| 657 |
; Successfully snapshotted #<STORE-OBJECT-SUBSYSTEM #xE6069EE> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 658 |
; Snapshotting subsystem #<BLOB-SUBSYSTEM #xE6069CE> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 659 |
; Successfully snapshotted #<BLOB-SUBSYSTEM #xE6069CE> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 660 |
; => NIL |
|---|
| 661 |
(restore) |
|---|
| 662 |
; restoring #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 663 |
; Restoring the subsystem #<STORE-OBJECT-SUBSYSTEM #xE6069EE> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 664 |
; loading snapshot file /tmp/object-store/current/store-object-subsystem-snapshot |
|---|
| 665 |
; Warning: internal inconsistency during restore: can't find store object 13 in loaded store |
|---|
| 666 |
; While executing: %DECODE-STORE-OBJECT, in process worker(1754). |
|---|
| 667 |
; Warning: Reference to inexistent object with id 13 from unnamed container, returning NIL. |
|---|
| 668 |
; While executing: %DECODE-STORE-OBJECT, in process worker(1754). |
|---|
| 669 |
; Restoring the subsystem #<BLOB-SUBSYSTEM #xE6069CE> of #<MP-STORE DIR: "/tmp/object-store/"> |
|---|
| 670 |
; loading transaction log /tmp/object-store/current/transaction-log |
|---|
| 671 |
; executing transaction #<ANONYMOUS-TRANSACTION 21.04.2008 08:48:48 PREPARE-FOR-SNAPSHOT NIL> at timestamp 3417749328 |
|---|
| 672 |
(relaxed-object-a (store-object-with-id 12)) |
|---|
| 673 |
; => NIL |
|---|
| 674 |
|
|---|
| 675 |
;;;# Store internals |
|---|
| 676 |
|
|---|
| 677 |
;;;## Binary data files |
|---|
| 678 |
;;; |
|---|
| 679 |
;;; This implementation of the BKNR datastore uses a binary encoding |
|---|
| 680 |
;;; of Lisp data. The encoding library is used by both the transaction |
|---|
| 681 |
;;; system and the object system and is mostly independent of |
|---|
| 682 |
;;; them. Users need not be aware of the details of this encoding, |
|---|
| 683 |
;;; except that (1) primitive data stored needs to be supported by the |
|---|
| 684 |
;;; encoding library and (2) user-defined object systems need to |
|---|
| 685 |
;;; register their own encoder and decoder methods to allow their |
|---|
| 686 |
;;; objects to be used as part of transaction arguments. |
|---|
| 687 |
|
|---|
| 688 |
Function ENCODE (OBJECT STREAM) |
|---|
| 689 |
|
|---|
| 690 |
Function DECODE (STREAM) =< OBJECT |
|---|
| 691 |
|
|---|
| 692 |
;;; The `STREAM' must be specialized on `(unsigned-byte 8)'. |
|---|
| 693 |
|
|---|
| 694 |
;;; The object store subsystem uses the encoding library to encode the |
|---|
| 695 |
;;; persistent state of all the objects in the store. It does this by |
|---|
| 696 |
;;; first serializing the layout of a class (which is a list of |
|---|
| 697 |
;;; slot-names), then by first serializing the class and the id of |
|---|
| 698 |
;;; each object, and finally by serializing the slots of each |
|---|
| 699 |
;;; object. This two-step system is necessary to correctly serialize |
|---|
| 700 |
;;; circular of forward references. |
|---|
| 701 |
|
|---|
| 702 |
;;; When the snapshot is loaded, an empty instance of each object is |
|---|
| 703 |
;;; created, and can be referenced only using the `ID'. After each |
|---|
| 704 |
;;; object has been instantiated, it can be referenced by another |
|---|
| 705 |
;;; object. The objects are serialized in the order they have been |
|---|
| 706 |
;;; created. |
|---|
| 707 |
|
|---|
| 708 |
;;;## Datastore state |
|---|
| 709 |
|
|---|
| 710 |
;;; The store always is in one of the following states: |
|---|
| 711 |
|
|---|
| 712 |
;;; :closed - No store is open, no persistent operations are |
|---|
| 713 |
;;; possible. It is an error, which will eventually be signaled, to |
|---|
| 714 |
;;; execute transactions in this state. |
|---|
| 715 |
|
|---|
| 716 |
;;; :restore - The store is currently recovering from a restart |
|---|
| 717 |
|
|---|
| 718 |
;;; :idle - The store has been opened and is prepared to execute |
|---|
| 719 |
;;; transactions. |
|---|
| 720 |
|
|---|
| 721 |
;;; :transaction - The store is currently executing a transaction |
|---|
| 722 |
|
|---|
| 723 |
;;; Note that all transactions are serialized and thus only one |
|---|
| 724 |
;;; transaction can be active at any time. |
|---|
| 725 |
|
|---|
| 726 |
;;; :snapshot - The store is currently writing a snapshot. |
|---|
| 727 |
|
|---|
| 728 |
;;;## Transactions |
|---|
| 729 |
|
|---|
| 730 |
;;; Transactions are objects of the class `TRANSACTION', and have a |
|---|
| 731 |
;;; slot containing the symbol of their transaction function, as well |
|---|
| 732 |
;;; as a list of the arguments that have to be passed to this |
|---|
| 733 |
;;; function. When a transaction is executed, a timestamp |
|---|
| 734 |
;;; of the execution time is stored in the object. |
|---|
| 735 |
|
|---|
| 736 |
;;; Transactions can be grouped by enclosing them with a |
|---|
| 737 |
;;; with-transaction block. All subtransactions will be logged to the |
|---|
| 738 |
;;; transaction log as a group and only re-executed on restore time |
|---|
| 739 |
;;; when the complete group could be read from the file. |
|---|
| 740 |
|
|---|
| 741 |
(defun foo-bar () |
|---|
| 742 |
(with-transaction () |
|---|
| 743 |
(invoke-transaction-a) |
|---|
| 744 |
(invoke-transaction-b))) |
|---|
| 745 |
|
|---|
| 746 |
;;; The persistent object store further supports the use of anonymous |
|---|
| 747 |
;;; transactions by automatically converting instance creation and |
|---|
| 748 |
;;; slot writes to transaction invocations which are atomically |
|---|
| 749 |
;;; executed upon restore time. |
|---|
| 750 |
|
|---|
| 751 |
;;;## Snapshot and restore procedures |
|---|
| 752 |
|
|---|
| 753 |
;;; When the datastore is snapshotted, the transaction layer ensures |
|---|
| 754 |
;;; that the store is opened, and that there are subsystems in the |
|---|
| 755 |
;;; store. Without subsystems, the transaction log is the only way for |
|---|
| 756 |
;;; the store to achieve persistence, and no snapshot can be made. The |
|---|
| 757 |
;;; store is then switched to read-only, and a backup directory is |
|---|
| 758 |
;;; created, containing the current transaction log and previous |
|---|
| 759 |
;;; snapshot files. This way, the older state of the datastore is not |
|---|
| 760 |
;;; lost. Then, each subsystem is asked to save its persistent state |
|---|
| 761 |
;;; by calling the method `SNAPSHOT-SUBSYSTEM'. When an error is |
|---|
| 762 |
;;; thrown during the snapshot of the subsystems, the backup directory |
|---|
| 763 |
;;; is renamed to be the current directory, and the store is |
|---|
| 764 |
;;; reopened. |
|---|
| 765 |
|
|---|
| 766 |
;;; When the datastore is restored, the store is switched to read |
|---|
| 767 |
;;; only, and each subsystem is asked to restore its persistent |
|---|
| 768 |
;;; state. Note that the subsystems are restored in the order in which |
|---|
| 769 |
;;; they are listed in the `SUBSYSTEMS' slot of the store, so that |
|---|
| 770 |
;;; dependent subsystems are restored last. When an error is thrown |
|---|
| 771 |
;;; while restoring the subsystems, the store is closed, and already |
|---|
| 772 |
;;; opened subsystems are closed using the method |
|---|
| 773 |
;;; `CLOSE-SUBSYSTEM'. After the restoring of all the subsystems, the |
|---|
| 774 |
;;; transaction log file is read, and each transaction recorded is |
|---|
| 775 |
;;; executed. This is where the `UNTIL' parameter comes into |
|---|
| 776 |
;;; play. Transactions that have been executed after the time of |
|---|
| 777 |
;;; `UNTIL' are discarded. |
|---|
| 778 |
|
|---|
| 779 |
;;;## Filesystem syncing |
|---|
| 780 |
|
|---|
| 781 |
;;; By default, the transaction log file is synced after a transaction |
|---|
| 782 |
;;; has been executed, so that all the data is correctly written on |
|---|
| 783 |
;;; disk. However, this can be a major performance stopper when |
|---|
| 784 |
;;; executing a big batch of transactions (for example, deleting a few |
|---|
| 785 |
;;; thousands objects). You can disable the mandatory syncing by |
|---|
| 786 |
;;; executing your transactions inside the form `WITHOUT-SYNC'. |
|---|
| 787 |
|
|---|
| 788 |
(without-sync () |
|---|
| 789 |
(execute-a-lot-of-transactions)) |
|---|
| 790 |
|
|---|
| 791 |
;;;## Snapshotting and restoring the object subsystem |
|---|
| 792 |
|
|---|
| 793 |
;;; Snapshotting and restoring the object subsystem is a bit tricky, |
|---|
| 794 |
;;; as additional systems come into play. When the object subsystem is |
|---|
| 795 |
;;; snapshotted using the method `SNAPSHOT-SUBSYSTEM', a snapshot file |
|---|
| 796 |
;;; containing a binary dump of all the current store objects is |
|---|
| 797 |
;;; created. First, the layouts of the objects (the name of their |
|---|
| 798 |
;;; slots) is stored, and minimal information about each object is |
|---|
| 799 |
;;; stored in the order of their creation (the minimal information |
|---|
| 800 |
;;; consists of the class of the object, and its ID). After this |
|---|
| 801 |
;;; information has been stored for each object, the slot values of |
|---|
| 802 |
;;; the store objects (again in the order of their creation) are |
|---|
| 803 |
;;; stored in the snapshot file. These two phases are necessary to |
|---|
| 804 |
;;; allow the snapshotting of circular or forward-referencing |
|---|
| 805 |
;;; structures. |
|---|
| 806 |
|
|---|
| 807 |
;;; When the object subsystem is restored, all the indices for classes |
|---|
| 808 |
;;; contained in the store are cleared in order to accomodate for the |
|---|
| 809 |
;;; new objects. Be very careful when using class indices that are not |
|---|
| 810 |
;;; related to store objects. The ID counter of the store subsystem is |
|---|
| 811 |
;;; reset to 0, and the class-layouts are read from the snapshot |
|---|
| 812 |
;;; file. Then, the minimal information for each object is read, and |
|---|
| 813 |
;;; an "empty version" of each object is instantiated. Thus, the |
|---|
| 814 |
;;; objects can be referenced by their ID. Then, the slot values for |
|---|
| 815 |
;;; each object are read from the snapshot file, references are |
|---|
| 816 |
;;; resolved (check the section about relaxed references). Finally, |
|---|
| 817 |
;;; after each slot value has been set, the method |
|---|
| 818 |
;;; `INITIALIZE-TRANSIENT-INSTANCE' is called for each created |
|---|
| 819 |
;;; object. The method `INITIALIZE-PERSISTENT-INSTANCE' is not called, |
|---|
| 820 |
;;; as it has to be executed only once at the time the persistent |
|---|
| 821 |
;;; object is created. |
|---|
| 822 |
|
|---|
| 823 |
;;;## Garbage collecting blobs |
|---|
| 824 |
|
|---|
| 825 |
;;; The binary data of deleted blob objects is kept in the blob root |
|---|
| 826 |
;;; directory by default. If you want to purge the binary data of |
|---|
| 827 |
;;; deleted objects, you can use the function |
|---|
| 828 |
;;; `DELETE-ORPHANED-BLOB-FILES'. However, take note that you won't be |
|---|
| 829 |
;;; able to restore the persistent state anteriour to the deletion of |
|---|
| 830 |
;;; the blobs, as their binary data is not stored in the transaction |
|---|
| 831 |
;;; log and not backed up by the snapshot method of the blob |
|---|
| 832 |
;;; subsystem. |
|---|
| 833 |
|
|---|
| 834 |
;;;## Schema evolution in the datastore |
|---|
| 835 |
|
|---|
| 836 |
;;; The transaction log only stores when a transaction is called, and |
|---|
| 837 |
;;; with which arguments. However, it doesn't store the definition of |
|---|
| 838 |
;;; the transaction itself. When the transaction definition is |
|---|
| 839 |
;;; changed, the transaction log may be restored in a different way, |
|---|
| 840 |
;;; according to the changes made in the code. |
|---|
| 841 |
;;; |
|---|
| 842 |
;;; In the same way, class definition changes are not recorded in the |
|---|
| 843 |
;;; transaction log. When a class definition is changed (for example a |
|---|
| 844 |
;;; slot initform is changed), the existing instances of the class are |
|---|
| 845 |
;;; updated accordingly. However, when the snapshot is restored in a |
|---|
| 846 |
;;; future session, the objects may be different than those created at |
|---|
| 847 |
;;; the last restore. |
|---|
| 848 |
;;; |
|---|
| 849 |
;;; The only way to cleanly upgrade transaction definitions and class |
|---|
| 850 |
;;; definitions is to make a snapshot after the changes have been |
|---|
| 851 |
;;; made. In a future version of the datastore, we hope to store all |
|---|
| 852 |
;;; the application sourcecode, so that a restore to a certain point |
|---|
| 853 |
;;; in time does not depend on the latest version of the code. The |
|---|
| 854 |
;;; object subsystem warns when a class definition is changed, and |
|---|
| 855 |
;;; urges the developer to make a snapshot of the database. Please be |
|---|
| 856 |
;;; careful, this can be a pretty tricky source of bugs. |
|---|