* registry.el (registry-db): Fix default registry-db max-size.
[gnus] / lisp / registry.el
1 ;;; registry.el --- Track and remember data items by various fields
2
3 ;; Copyright (C) 2011-2014 Free Software Foundation, Inc.
4
5 ;; Author: Teodor Zlatanov <tzz@lifelogs.com>
6 ;; Keywords: data
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This library provides a general-purpose EIEIO-based registry
26 ;; database with persistence, initialized with these fields:
27
28 ;; version: a float
29
30 ;; max-size: an integer, default 5000000
31
32 ;; prune-factor: a float between 0 and 1, default 0.1
33
34 ;; precious: a list of symbols
35
36 ;; tracked: a list of symbols
37
38 ;; tracker: a hashtable tuned for 100 symbols to track (you should
39 ;; only access this with the :lookup2-function and the
40 ;; :lookup2+-function)
41
42 ;; data: a hashtable with default size 10K and resize threshold 2.0
43 ;; (this reflects the expected usage so override it if you know better)
44
45 ;; ...plus methods to do all the work: `registry-search',
46 ;; `registry-lookup', `registry-lookup-secondary',
47 ;; `registry-lookup-secondary-value', `registry-insert',
48 ;; `registry-delete', `registry-prune', `registry-size' which see
49
50 ;; and with the following properties:
51
52 ;; Every piece of data has a unique ID and some general-purpose fields
53 ;; (F1=D1, F2=D2, F3=(a b c)...) expressed as an alist, e.g.
54
55 ;; ((F1 D1) (F2 D2) (F3 a b c))
56
57 ;; Note that whether a field has one or many pieces of data, the data
58 ;; is always a list of values.
59
60 ;; The user decides which fields are "precious", F2 for example.  When
61 ;; the registry is pruned, any entries without the F2 field will be
62 ;; removed until the size is :max-size * :prune-factor _less_ than the
63 ;; maximum database size. No entries with the F2 field will be removed
64 ;; at PRUNE TIME, which means it may not be possible to prune back all
65 ;; the way to the target size.
66
67 ;; When an entry is inserted, the registry will reject new entries if
68 ;; they bring it over the :max-size limit, even if they have the F2
69 ;; field.
70
71 ;; The user decides which fields are "tracked", F1 for example.  Any
72 ;; new entry is then indexed by all the tracked fields so it can be
73 ;; quickly looked up that way.  The data is always a list (see example
74 ;; above) and each list element is indexed.
75
76 ;; Precious and tracked field names must be symbols.  All other
77 ;; fields can be any other Emacs Lisp types.
78
79 ;;; Code:
80
81 (eval-when-compile (require 'cl))
82
83 (eval-and-compile
84   (or (ignore-errors (progn
85                        (require 'eieio)
86                        (require 'eieio-base)))
87       ;; gnus-fallback-lib/ from gnus/lisp/gnus-fallback-lib
88       (ignore-errors
89         (let ((load-path (cons (expand-file-name
90                                 "gnus-fallback-lib/eieio"
91                                 (file-name-directory (locate-library "gnus")))
92                                load-path)))
93           (require 'eieio)
94           (require 'eieio-base)))
95       (error
96        "eieio not found in `load-path' or gnus-fallback-lib/ directory.")))
97
98 ;; The version number needs to be kept outside of the class definition
99 ;; itself.  The persistent-save process does *not* write to file any
100 ;; slot values that are equal to the default :initform value.  If a
101 ;; database object is at the most recent version, therefore, its
102 ;; version number will not be written to file.  That makes it
103 ;; difficult to know when a database needs to be upgraded.
104 (defvar registry-db-version 0.2
105   "The current version of the registry format.")
106
107 (defclass registry-db (eieio-persistent)
108   ((version :initarg :version
109             :initform nil
110             :type (or null float)
111             :documentation "The registry version.")
112    (max-size :initarg :max-size
113              :initform 5000000
114              :type integer
115              :custom integer
116              :documentation "The maximum number of registry entries.")
117    (prune-factor
118     :initarg :prune-factor
119     :initform 0.1
120     :type float
121     :custom float
122     :documentation "Prune to \(:max-size * :prune-factor\) less
123     than the :max-size limit.  Should be a float between 0 and 1.")
124    (tracked :initarg :tracked
125             :initform nil
126             :type t
127             :documentation "The tracked (indexed) fields, a list of symbols.")
128    (precious :initarg :precious
129              :initform nil
130              :type t
131              :documentation "The precious fields, a list of symbols.")
132    (tracker :initarg :tracker
133             :type hash-table
134             :documentation "The field tracking hashtable.")
135    (data :initarg :data
136          :type hash-table
137          :documentation "The data hashtable.")))
138
139 (defmethod initialize-instance :BEFORE ((this registry-db) slots)
140   "Check whether a registry object needs to be upgraded."
141   ;; Hardcoded upgrade routines.  Version 0.1 to 0.2 requires the
142   ;; :max-soft slot to disappear, and the :max-hard slot to be renamed
143   ;; :max-size.
144   (let ((current-version
145          (and (plist-member slots :version)
146               (plist-get slots :version))))
147     (when (or (null current-version)
148               (eql current-version 0.1))
149       (setq slots
150             (plist-put slots :max-size (plist-get slots :max-hard)))
151       (setq slots
152             (plist-put slots :version registry-db-version))
153       (cl-remf slots :max-hard)
154       (cl-remf slots :max-soft))))
155
156 (defmethod initialize-instance :AFTER ((this registry-db) slots)
157   "Set value of data slot of THIS after initialization."
158   (with-slots (data tracker) this
159     (unless (member :data slots)
160       (setq data
161             (make-hash-table :size 10000 :rehash-size 2.0 :test 'equal)))
162     (unless (member :tracker slots)
163       (setq tracker (make-hash-table :size 100 :rehash-size 2.0)))))
164
165 (defmethod registry-lookup ((db registry-db) keys)
166   "Search for KEYS in the registry-db THIS.
167 Returns an alist of the key followed by the entry in a list, not a cons cell."
168   (let ((data (oref db :data)))
169     (delq nil
170           (mapcar
171            (lambda (k)
172              (when (gethash k data)
173                (list k (gethash k data))))
174            keys))))
175
176 (defmethod registry-lookup-breaks-before-lexbind ((db registry-db) keys)
177   "Search for KEYS in the registry-db THIS.
178 Returns an alist of the key followed by the entry in a list, not a cons cell."
179   (let ((data (oref db :data)))
180     (delq nil
181           (loop for key in keys
182                 when (gethash key data)
183                 collect (list key (gethash key data))))))
184
185 (defmethod registry-lookup-secondary ((db registry-db) tracksym
186                                       &optional create)
187   "Search for TRACKSYM in the registry-db THIS.
188 When CREATE is not nil, create the secondary index hashtable if needed."
189   (let ((h (gethash tracksym (oref db :tracker))))
190     (if h
191         h
192       (when create
193         (puthash tracksym
194                  (make-hash-table :size 800 :rehash-size 2.0 :test 'equal)
195                  (oref db :tracker))
196         (gethash tracksym (oref db :tracker))))))
197
198 (defmethod registry-lookup-secondary-value ((db registry-db) tracksym val
199                                             &optional set)
200   "Search for TRACKSYM with value VAL in the registry-db THIS.
201 When SET is not nil, set it for VAL (use t for an empty list)."
202   ;; either we're asked for creation or there should be an existing index
203   (when (or set (registry-lookup-secondary db tracksym))
204     ;; set the entry if requested,
205     (when set
206       (puthash val (if (eq t set) '() set)
207                (registry-lookup-secondary db tracksym t)))
208     (gethash val (registry-lookup-secondary db tracksym))))
209
210 (defun registry--match (mode entry check-list)
211   ;; for all members
212   (when check-list
213     (let ((key (nth 0 (nth 0 check-list)))
214           (vals (cdr-safe (nth 0 check-list)))
215           found)
216       (while (and key vals (not found))
217         (setq found (case mode
218                       (:member
219                        (member (car-safe vals) (cdr-safe (assoc key entry))))
220                       (:regex
221                        (string-match (car vals)
222                                      (mapconcat
223                                       'prin1-to-string
224                                       (cdr-safe (assoc key entry))
225                                       "\0"))))
226               vals (cdr-safe vals)))
227       (or found
228           (registry--match mode entry (cdr-safe check-list))))))
229
230 (defmethod registry-search ((db registry-db) &rest spec)
231   "Search for SPEC across the registry-db THIS.
232 For example calling with :member '(a 1 2) will match entry '((a 3 1)).
233 Calling with :all t (any non-nil value) will match all.
234 Calling with :regex '\(a \"h.llo\") will match entry '((a \"hullo\" \"bye\").
235 The test order is to check :all first, then :member, then :regex."
236   (when db
237     (let ((all (plist-get spec :all))
238           (member (plist-get spec :member))
239           (regex (plist-get spec :regex)))
240       (loop for k being the hash-keys of (oref db :data)
241             using (hash-values v)
242             when (or
243                   ;; :all non-nil returns all
244                   all
245                   ;; member matching
246                   (and member (registry--match :member v member))
247                   ;; regex matching
248                   (and regex (registry--match :regex v regex)))
249             collect k))))
250
251 (defmethod registry-delete ((db registry-db) keys assert &rest spec)
252   "Delete KEYS from the registry-db THIS.
253 If KEYS is nil, use SPEC to do a search.
254 Updates the secondary ('tracked') indices as well.
255 With assert non-nil, errors out if the key does not exist already."
256   (let* ((data (oref db :data))
257          (keys (or keys
258                    (apply 'registry-search db spec)))
259          (tracked (oref db :tracked)))
260
261     (dolist (key keys)
262       (let ((entry (gethash key data)))
263         (when assert
264           (assert entry nil
265                   "Key %s does not exist in database" key))
266         ;; clean entry from the secondary indices
267         (dolist (tr tracked)
268           ;; is this tracked symbol indexed?
269           (when (registry-lookup-secondary db tr)
270             ;; for every value in the entry under that key...
271             (dolist (val (cdr-safe (assq tr entry)))
272               (let* ((value-keys (registry-lookup-secondary-value
273                                   db tr val)))
274                 (when (member key value-keys)
275                   ;; override the previous value
276                   (registry-lookup-secondary-value
277                    db tr val
278                    ;; with the indexed keys MINUS the current key
279                    ;; (we pass t when the list is empty)
280                    (or (delete key value-keys) t)))))))
281         (remhash key data)))
282     keys))
283
284 (defmethod registry-size ((db registry-db))
285   "Returns the size of the registry-db object THIS.
286 This is the key count of the :data slot."
287   (hash-table-count (oref db :data)))
288
289 (defmethod registry-full ((db registry-db))
290   "Checks if registry-db THIS is full."
291   (>= (registry-size db)
292       (oref db :max-size)))
293
294 (defmethod registry-insert ((db registry-db) key entry)
295   "Insert ENTRY under KEY into the registry-db THIS.
296 Updates the secondary ('tracked') indices as well.
297 Errors out if the key exists already."
298
299   (assert (not (gethash key (oref db :data))) nil
300           "Key already exists in database")
301
302   (assert (not (registry-full db))
303           nil
304           "registry max-size limit reached")
305
306   ;; store the entry
307   (puthash key entry (oref db :data))
308
309   ;; store the secondary indices
310   (dolist (tr (oref db :tracked))
311     ;; for every value in the entry under that key...
312     (dolist (val (cdr-safe (assq tr entry)))
313       (let* ((value-keys (registry-lookup-secondary-value db tr val)))
314         (pushnew key value-keys :test 'equal)
315         (registry-lookup-secondary-value db tr val value-keys))))
316   entry)
317
318 (defmethod registry-reindex ((db registry-db))
319   "Rebuild the secondary indices of registry-db THIS."
320   (let ((count 0)
321         (expected (* (length (oref db :tracked)) (registry-size db))))
322     (dolist (tr (oref db :tracked))
323       (let (values)
324         (maphash
325          (lambda (key v)
326            (incf count)
327            (when (and (< 0 expected)
328                       (= 0 (mod count 1000)))
329              (message "reindexing: %d of %d (%.2f%%)"
330                       count expected (/ (* 100 count) expected)))
331            (dolist (val (cdr-safe (assq tr v)))
332              (let* ((value-keys (registry-lookup-secondary-value db tr val)))
333                (push key value-keys)
334                (registry-lookup-secondary-value db tr val value-keys))))
335          (oref db :data))))))
336
337 (defmethod registry-prune ((db registry-db) &optional sortfunc)
338   "Prunes the registry-db object DB.
339
340 Attempts to prune the number of entries down to \(*
341 :max-size :prune-factor\) less than the max-size limit, so
342 pruning doesn't need to happen on every save. Removes only
343 entries without the :precious keys, so it may not be possible to
344 reach the target limit.
345
346 Entries to be pruned are first sorted using SORTFUNC.  Entries
347 from the front of the list are deleted first.
348
349 Returns the number of deleted entries."
350   (let ((size (registry-size db))
351         (target-size (- (oref db :max-size)
352                         (* (oref db :max-size)
353                            (oref db :prune-factor))))
354         candidates)
355     (if (> size target-size)
356         (progn
357           (setq candidates
358                 (registry-collect-prune-candidates
359                  db (- size target-size) sortfunc))
360           (length (registry-delete db candidates nil)))
361       0)))
362
363 (defmethod registry-collect-prune-candidates ((db registry-db) limit sortfunc)
364   "Collects pruning candidates from the registry-db object DB.
365
366 Proposes only entries without the :precious keys, and attempts to
367 return LIMIT such candidates.  If SORTFUNC is provided, sort
368 entries first and return candidates from beginning of list."
369   (let* ((precious (oref db :precious))
370          (precious-p (lambda (entry-key)
371                        (cdr (memq (car entry-key) precious))))
372          (data (oref db :data))
373          (candidates (cl-loop for k being the hash-keys of data
374                               using (hash-values v)
375                               when (notany precious-p v)
376                               collect (cons k v))))
377     ;; We want the full entries for sorting, but should only return a
378     ;; list of entry keys.
379     (when sortfunc
380       (setq candidates (sort candidates sortfunc)))
381     (delq nil (cl-subseq (mapcar #'car candidates) 0 limit))))
382
383 (provide 'registry)
384 ;;; registry.el ends here