Coverage report: /home/smo/lisp/release/cl-swap-file_0.2/swap-file.lisp
| Kind | Covered | All | % |
| expression | 626 | 713 | 87.8 |
| branch | 42 | 54 | 77.8 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
(in-package :swap-file)
3
(declaim (optimize (space 0) (speed 0) (debug 3) (safety 2))) ; development mode
6
(version 1 :type (unsigned-byte 32))
7
(block-size 4096 :type (unsigned-byte 32))
8
(next-new-offset 0 :type (unsigned-byte 32))
9
(available-list 0 :type (unsigned-byte 32)) ; offset to first available block or 0 if none (= need to create one)
10
(block-cache nil :type list) ; list of weak pointers pointing to a disk blocks.
11
(open-blocks nil :type list) ; list of currently open disk blocks. This is cleared after flush.
12
(journal nil :type (or wal:wal null))
13
(stream nil :type stream))
16
(offset 0 :type (unsigned-byte 32)) ; file position
17
(max-data 0 :type (unsigned-byte 32)) ; maximum length of data to fit in this block
18
(deleted-p nil :type t) ; 1 byte on disk, t = 0, nil = 1
19
(next 0 :type (unsigned-byte 32)) ; 4 bytes
20
(data-size 0 :type (unsigned-byte 32)) ; 4 bytes
21
(data nil :type (or array null)))
23
(defun available-p (swap-file)
24
"Returns t if there is available (deleted) blocks on swap-file, nil otherwise."
25
(/= (swap-file-available-list swap-file) 0))
27
(defun max-data-size (block-size)
28
"Returns maximum size of payload data. This size block-size reduced by the size of block header."
29
(- block-size 1 4 4)) ;; deleted-p = 1 byte, next = 4 bytes, data-size = 4 bytes
31
(defun make-empty-data (swap-file)
32
"Returns an empty array of bytes. Array's initial size is max-data-size but the array is adjustable."
33
(make-array (max-data-size (swap-file-block-size swap-file)) :adjustable t :fill-pointer 0 :element-type 'unsigned-byte :initial-element 0))
35
(defun make-data-array (swap-file initial-contents)
36
"Returns an array of bytes with given initial contents."
37
(map-into (make-array (max (length initial-contents) (max-data-size (swap-file-block-size swap-file))) :adjustable t :fill-pointer t :element-type 'unsigned-byte :initial-element 0) #'identity initial-contents))
40
(defun wal-entry-writer (entry stream)
41
"Writes a journal entry to a stream. The function is writer for write-ahead log."
42
;; write master offset
43
(little-endian:write-uint32 (car entry) stream)
44
;; write sequence size
45
(little-endian:write-uint32 (array-dimension (cdr entry) 0) stream)
47
(write-sequence (cdr entry) stream))
49
(defun wal-entry-reader (stream)
50
"Reads a journal entry from stream. The function is reader for write-ahead log."
52
(cons (little-endian:read-uint32 stream)
53
(read-sequence (make-array (little-endian:read-uint32 stream)
54
:element-type 'unsigned-byte)
57
(defun make-master-writer (swap-file)
58
"Returns master writer function. The master writer is called by
59
write-ahead log when an entry is committed to swap-file's stream.
60
Master writer locates entry's file position and writes entry's
61
contents to that position."
62
(let ((swap-file swap-file))
64
(file-position (swap-file-stream swap-file) (car entry))
65
(write-sequence (cdr entry) (swap-file-stream swap-file)))))
66
;; (finish-output (swap-file-stream swap-file)))))
67
;; (write-disk-block disk-block swap-file))))
69
(defun write-sequence-to-disk (data swap-file)
70
"Write sequence to disk. Sequence is written to a write ahead log."
71
(wal:write (swap-file-journal swap-file) (cons (file-position (swap-file-stream swap-file))
73
;; (write-sequence data (swap-file-stream swap-file)))
75
(defun write-uint32-to-disk (value swap-file)
76
"Write 32-bit unsigned byte to disk. Value is written to a write ahead log."
77
(write-sequence-to-disk
78
(binary-file:with-output-to-binary-array (out)
79
(little-endian:write-uint32 value out))
82
(defun write-uint8-to-disk (value swap-file)
83
"Write 8-bit unsigned byte to disk. Value is written to a write ahead log."
84
(write-sequence-to-disk
85
(little-endian:write-uint8 value (swap-file-stream swap-file))
88
(defun write-swap-file-header (swap-file)
89
"Write swap-file's header to a swap-file's stream."
91
(binary-file:with-output-to-binary-array (s)
92
(little-endian:write-uint8 (char-code #\S) s)
93
(little-endian:write-uint8 (char-code #\W) s)
94
(little-endian:write-uint8 (char-code #\A) s)
95
(little-endian:write-uint8 (char-code #\P) s)
96
(little-endian:write-uint32 (swap-file-version swap-file) s)
97
(little-endian:write-uint32 (swap-file-block-size swap-file) s)
98
(little-endian:write-uint32 (swap-file-next-new-offset swap-file) s)
99
(little-endian:write-uint32 (swap-file-available-list swap-file) s)
100
;; pad 0's till end of the first block.
101
(write-sequence (make-array (- (swap-file-block-size swap-file) (swap-file-header-size)) :initial-element 0 :element-type 'unsigned-byte) s))
102
(swap-file-stream swap-file))
103
(finish-output (swap-file-stream swap-file))
106
(defun read-swap-file-next-new-offset (swap-file)
107
"Reads and returns offset of the next new disk block."
108
(file-position (swap-file-stream swap-file) 12)
109
(little-endian:read-uint32 (swap-file-stream swap-file)))
111
(defun write-swap-file-next-new-offset (swap-file)
112
"Writes offset of the next new disk block to swap-file's stream."
113
(file-position (swap-file-stream swap-file) 12)
114
(write-uint32-to-disk (swap-file-next-new-offset swap-file) swap-file))
116
(defun read-swap-file-first-available (swap-file)
117
"Reads and returns offset of first available (deleted) disk block. If
118
there is no available blocks, returns 0."
119
(file-position (swap-file-stream swap-file) 16)
120
(little-endian:read-uint32 (swap-file-stream swap-file)))
122
(defun write-swap-file-first-available (offset swap-file)
123
"Writes offset of first available (deleted) disk block to the
125
(file-position (swap-file-stream swap-file) 16)
126
(write-uint32-to-disk offset swap-file))
128
(defun read-swap-file-header (swap-file)
129
"Reads swap-file header and updates swap-file structure. Returns updated swap-file structure."
130
(unless (and (= (little-endian:read-uint8 (swap-file-stream swap-file)) (char-code #\S))
131
(= (little-endian:read-uint8 (swap-file-stream swap-file)) (char-code #\W))
132
(= (little-endian:read-uint8 (swap-file-stream swap-file)) (char-code #\A))
133
(= (little-endian:read-uint8 (swap-file-stream swap-file)) (char-code #\P)))
134
(error "Not a swap file."))
135
(unless (= (swap-file-version swap-file) (little-endian:read-uint32 (swap-file-stream swap-file)))
136
(error "Swap file version does not match."))
137
(setf (swap-file-block-size swap-file) (little-endian:read-uint32 (swap-file-stream swap-file))
138
(swap-file-next-new-offset swap-file) (little-endian:read-uint32 (swap-file-stream swap-file))
139
(swap-file-available-list swap-file) (little-endian:read-uint32 (swap-file-stream swap-file)))
142
(defun swap-file-header-size ()
143
"Returns size of swap-file header."
148
4 ; next available offset
151
(defun set-block-data (disk-block data)
152
"Sets disk-blocks data. Returns updated disk-block."
153
(setf (disk-block-data disk-block) data)
154
(if (array-has-fill-pointer-p data)
155
(setf (disk-block-data-size disk-block) (fill-pointer data))
156
(setf (disk-block-data-size disk-block) (length data)))
159
(defun allocate-new-block (swap-file &optional (data (make-empty-data swap-file)))
160
"Returns new empty block from swap file. Disk block is allocated
161
from end of the file if none is available. Empty block data array
162
contains only zeros and fill pointer is set to 0. If data is given,
163
the given data is to allocated block but not flushed to the disk. In
164
that case caller should call write-disk-block after allocation."
165
(declare (optimize (speed 0) (space 0) (debug 3)))
166
(let ((new-block (make-disk-block :offset (swap-file-next-new-offset swap-file)
167
;;(file-position (swap-file-stream swap-file))
168
:max-data (max-data-size (swap-file-block-size swap-file))
171
:data-size (fill-pointer data)
174
;; write all 0's to new block.
175
(file-position (swap-file-stream swap-file) (swap-file-next-new-offset swap-file))
176
(write-sequence-to-disk (make-array (swap-file-block-size swap-file) :element-type 'unsigned-byte :initial-element 0)
179
;; increment next new offset
180
(incf (swap-file-next-new-offset swap-file) (swap-file-block-size swap-file))
181
(write-swap-file-next-new-offset swap-file)
185
(defun append-to-available (disk-block swap-file)
186
"Pushes given disk-block to swap-file's available list. Available
187
list is updated at disk."
188
;; link block to available list.
189
(setf (disk-block-next disk-block) (swap-file-available-list swap-file))
190
(setf (swap-file-available-list swap-file) (disk-block-offset disk-block))
191
(write-swap-file-first-available (disk-block-offset disk-block) swap-file)
193
;; seek to disk block
194
(file-position (swap-file-stream swap-file) (disk-block-offset disk-block))
196
;; mark block as deleted
197
(setf (disk-block-deleted-p disk-block) t)
198
(write-uint8-to-disk 1 swap-file)
200
;; write next available to disk
201
(write-uint32-to-disk (disk-block-next disk-block) swap-file)
204
(defun get-first-available (swap-file)
205
"Returns first available (deleted) disk block or nil if there is no available disk blocks."
206
(when (available-p swap-file)
207
(let ((disk-block (read-disk-block swap-file (swap-file-available-list swap-file))))
208
(setf (swap-file-available-list swap-file) (disk-block-next disk-block))
209
(write-swap-file-first-available (disk-block-next disk-block) swap-file)
211
;; seek to disk block
212
(file-position (swap-file-stream swap-file) (disk-block-offset disk-block))
214
;; mark block as undeleted
215
(setf (disk-block-deleted-p disk-block) nil)
216
(write-uint8-to-disk 0 swap-file)
218
;; write next available to disk
219
(setf (disk-block-next disk-block) 0)
220
(write-uint32-to-disk 0 swap-file)
223
(defun read-disk-block-array (stream block-size)
224
"Reads and returns (single) disk block's data as an array of bytes."
225
(let ((array (make-array block-size :element-type 'unsigned-byte :initial-element 0))
227
(setq result (read-sequence array stream))
228
(assert (= block-size result))
231
(defun read-disk-block (swap-file offset)
232
"Reads and returns disk block from swap file. If disk node is marked as deleted, the
234
(file-position (swap-file-stream swap-file) offset)
235
(binary-file:with-input-from-binary-array (s (read-disk-block-array (swap-file-stream swap-file)
236
(swap-file-block-size swap-file)))
237
(let ((disk-block (make-disk-block :offset offset
238
:max-data (max-data-size (swap-file-block-size swap-file))
239
:deleted-p (= (read-byte s) 1)
240
:next (little-endian:read-uint32 s)
241
:data-size (little-endian:read-uint32 s)
242
:data (make-empty-data swap-file))))
243
(unless (disk-block-deleted-p disk-block)
244
(setf (fill-pointer (disk-block-data disk-block)) (disk-block-data-size disk-block))
245
(read-sequence (disk-block-data disk-block) s :start 0))
248
(defun read-connected-blocks (swap-file offset)
249
"Returns a list of connected disk blocks read from swap-file starting from disk block at given offset."
250
(let ((disk-block (open-block swap-file offset)))
251
(if (= (disk-block-next disk-block) 0)
253
(cons disk-block (read-connected-blocks swap-file (disk-block-next disk-block))))))
255
(defun merge-data (data-chunks)
256
"Returns a new adjustable array with fill pointer. Arrays size is
257
equal to sum of given data chunks and contents are merged from data
258
chunks. Data chunks are arrays themselves."
260
(let ((new-data (make-array (reduce #'+ data-chunks :key #'data-size :initial-value 0)
261
:initial-element 0 :element-type 'unsigned-byte :adjustable t :fill-pointer t))
263
(mapcar #'(lambda (data-chunk)
264
(setf (subseq new-data start (+ start (data-size data-chunk))) data-chunk)
265
(incf start (data-size data-chunk)))
269
(defun data-size-below-max (disk-block)
270
"Returns disk blocks data size or disk block maximum data size if
271
current disk block's data size is over than maximum."
272
(min (disk-block-data-size disk-block)
273
(disk-block-max-data disk-block)))
275
(defgeneric data-size (obj)
276
(:method ((obj disk-block))
277
(disk-block-data-size obj))
278
(:method ((obj array))
279
(if (array-has-fill-pointer-p obj)
283
(defun next-p (disk-block)
284
"Returns t if disk bock has next block set, nil otherwise."
285
(/= (disk-block-next disk-block) 0))
287
(defun split-data (data max-size)
288
"Returns data splitted to a list of byte arrays with each having size below max-size."
290
(if (= (length data) 0)
292
(do ((pos 0 (+ pos max-size))
294
((>= pos (length data)) (reverse chunks))
295
(push (make-array (min (- (length data) pos) max-size) :displaced-to data :displaced-index-offset pos) chunks)))))
297
(defun offset (disk-block)
298
"Returns disk block's offset or 0 if disk block is nil."
300
(disk-block-offset disk-block)
303
(defun write-connected-blocks (data-chunks swap-file offset)
304
"Writes data splitted to data-chunks to the swap-file starting from
305
disk block at given offset. The old disk block contents are
306
overwritten. If the data is shorter than old data the exceeding disk
307
blocks are deleted. If the data is longer than old data new disk
308
blocks are allocated as needed."
310
((and (null data-chunks) (= offset 0))
312
;; unlink old connected blocks if there is no more data left.
313
((and (null data-chunks) (> offset 0))
314
(unlink-block swap-file offset)
316
;; allocate new block.
317
((and data-chunks (= offset 0))
318
(write-disk-block (create-block swap-file :next (offset (write-connected-blocks (rest data-chunks) swap-file 0)) :data (first data-chunks)) swap-file))
319
;; write over old connected block.
320
(t ; (and data-chunks (> offset 0))
321
(let ((disk-block (open-block swap-file offset)))
322
(setf (disk-block-next disk-block) (offset (write-connected-blocks (rest data-chunks) swap-file (disk-block-next disk-block))))
323
(write-disk-block (set-block-data (open-block swap-file offset) (first data-chunks)) swap-file)))))
325
;; (defun delete-extending (disk-block swap-file)
326
;; "Unlinks extending blocks from given disk block, if such exist."
327
;; (when (next-p disk-block)
328
;; (unlink-block swap-file (open-block swap-file (disk-block-next disk-block)))
329
;; (setf (disk-block-next disk-block) 0)))
331
(defun write-disk-block (disk-block swap-file)
332
"Write disk block to swap file. The disk block must be allocated
333
before it can be written. If the data array contents exceed block
334
size new blocks are allocated until all data can fit and also
335
exceeding data is written to the disk."
337
;; if sequence is over max data, write extending data to existing or new extending block.
338
;; this called first because we want to get next block's offset before writing
339
;; current block on disk.
340
;; (if (> (fill-pointer (disk-block-data disk-block)) (disk-block-max-data disk-block))
341
;; (write-extending disk-block swap-file)
342
;; (delete-extending disk-block swap-file))
344
;; locate disk-block on swap-file
345
(file-position (swap-file-stream swap-file) (disk-block-offset disk-block))
347
;; write disk block to disk.
348
(write-sequence-to-disk
349
;; construct full disk block array using in memory stream.
350
(binary-file:with-output-to-binary-array (out)
351
(let ((allowed-size (data-size-below-max disk-block)))
353
(write-byte (if (disk-block-deleted-p disk-block) 1 0) out)
354
;; write next block offset
355
(little-endian:write-uint32 (disk-block-next disk-block) out)
357
(little-endian:write-uint32 allowed-size out)
359
(write-sequence (disk-block-data disk-block) out :end allowed-size)))
364
;;; exported functions
367
(defun flush (swap-file)
368
"Forces all written changes to be written on to swap-file's file."
369
(if (swap-file-journal swap-file)
370
(wal:commit (swap-file-journal swap-file)))
371
(finish-output (swap-file-stream swap-file))
372
(setf (swap-file-open-blocks swap-file) nil)
375
(defun rollback (swap-file)
376
"Discard all changes to a swap-file."
377
(wal:rollback (swap-file-journal swap-file))
380
(defun create-journal (swap-file)
381
"Create journal for a swap-file."
382
(setf (swap-file-journal swap-file) (wal:open (swap-file-stream swap-file) (make-master-writer swap-file) :if-exists :error :if-does-not-exist :create :entry-writer #'wal-entry-writer :entry-reader #'wal-entry-reader))
385
(defun open-journal (swap-file)
386
"Open existing journal for a swap-file."
387
(setf (swap-file-journal swap-file) (wal:open (swap-file-stream swap-file) (make-master-writer swap-file) :if-exists :overwrite :if-does-not-exist :error :entry-writer #'wal-entry-writer :entry-reader #'wal-entry-reader))
388
(wal:recover (swap-file-journal swap-file))
391
(defun create (filespec &key (if-exists :error) (block-size 4096))
392
"Creates and returns new swap-file. Swap-file is created to given filespec."
393
(assert (>= block-size 20))
394
(write-swap-file-header
395
(create-journal (make-swap-file :version 1 :block-size block-size :next-new-offset block-size :stream (binary-file:open-binary-stream filespec :if-exists if-exists :if-does-not-exist :create)))))
397
(defun open (filespec &key (if-exists :overwrite) (if-does-not-exist :error))
398
"Opens and returns existing swap-file."
400
(read-swap-file-header (make-swap-file :version 1 :stream (binary-file:open-binary-stream filespec :if-exists if-exists :if-does-not-exist if-does-not-exist)))))
402
(defun close (swap-file)
405
(cl:close (swap-file-stream swap-file)))
407
(defun push-to-cache (swap-file disk-block)
408
"Push disk block to swap-file disk block cache."
409
(push (sb-ext:make-weak-pointer disk-block) (swap-file-block-cache swap-file))
412
(defun get-from-cache (swap-file offset)
413
"Returns disk block from cache, or nil if disk block with given
414
offset does not exist."
415
(do* ((pos (swap-file-block-cache swap-file) (cdr pos))
418
(let ((v (sb-ext:weak-pointer-value (car pos))))
419
(when (and v (= (disk-block-offset v) offset))
422
(defun create-block (swap-file &key (next 0) (data (make-empty-data swap-file)))
423
"Allocates and returns empty disk block from swap-file."
425
(if (available-p swap-file)
426
(push-to-cache swap-file (get-first-available swap-file))
427
(push-to-cache swap-file (allocate-new-block swap-file)))))
428
(setf (disk-block-next disk-block) next)
429
(push disk-block (swap-file-open-blocks swap-file))
430
(set-block-data disk-block data)))
432
(defun open-block (swap-file offset)
433
"Opens and returns existing disk block from swap-file."
434
(let ((disk-block (get-from-cache swap-file offset)))
436
(setq disk-block (push-to-cache swap-file (read-disk-block swap-file offset))))
437
(push disk-block (swap-file-open-blocks swap-file))
440
(defun read-data (swap-file offset)
441
"Returns an adjustable array with fill pointer. Function read-data
442
reads connected disk blocks from swap-file starting from given
443
offset. If disk block extends to next block, the data of consequtive
444
blocks are automatically appended to the data array of the read
445
block. In other words, returned data array may contain data from
446
multiple disk blocks."
447
(merge-data (mapcar #'disk-block-data (read-connected-blocks swap-file offset))))
449
(defun write-data (data swap-file offset)
450
"Write data to a swap-file and to a given offset."
451
(write-connected-blocks (split-data data (max-data-size (swap-file-block-size swap-file)))
455
(defun close-block (swap-file disk-block)
457
(write-disk-block disk-block swap-file))
458
;; (wal:write (swap-file-journal swap-file) disk-block))
460
(defun ensure-disk-block (disk-block-spec swap-file)
461
"Returns or opens and returns a disk block."
462
(etypecase disk-block-spec
463
(disk-block disk-block-spec)
464
(integer (open-block swap-file disk-block-spec))))
466
(defun unlink-block (swap-file disk-block-spec &aux (disk-block (ensure-disk-block disk-block-spec swap-file)))
467
"Unlinks disk block. Disk block is pushed into available disk blocks list."
468
(let ((next (disk-block-next disk-block)))
469
;; link block to available list
470
(append-to-available disk-block swap-file)
472
;; any extending blocks are also deleted
474
(unlink-block swap-file (open-block swap-file next))
477
(defun set-file-position-after-header (swap-file &optional (offset 0))
478
"Sets swap file's file position after swap file header. If offset is
479
given, the offset is added to file position. The file position after
480
header is reserved for application meta data. Available meta data size
481
is block size reduced by header size."
482
(file-position (swap-file-stream swap-file) (+ (swap-file-header-size) offset)))