diff options
| author | Ted Zlatanov | 2011-04-05 23:37:02 +0000 |
|---|---|---|
| committer | Katsumi Yamaoka | 2011-04-05 23:37:02 +0000 |
| commit | ccd58722df74090fc1968db5b9cb42f8d43e4a0b (patch) | |
| tree | 91e62ddf98a19858dc4459456b90f6be7aa6f0c1 | |
| parent | 11a3174d87f3a09fe0d9d36d41669cf5d66e3019 (diff) | |
| download | emacs-ccd58722df74090fc1968db5b9cb42f8d43e4a0b.tar.gz emacs-ccd58722df74090fc1968db5b9cb42f8d43e4a0b.zip | |
Add lisp/gnus/registry.el.
| -rw-r--r-- | lisp/gnus/registry.el | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/lisp/gnus/registry.el b/lisp/gnus/registry.el new file mode 100644 index 00000000000..3c4457d8577 --- /dev/null +++ b/lisp/gnus/registry.el | |||
| @@ -0,0 +1,409 @@ | |||
| 1 | ;;; registry.el --- Track and remember data items by various fields | ||
| 2 | |||
| 3 | ;; Copyright (C) 2011 Teodor Zlatanov | ||
| 4 | |||
| 5 | ;; Author: Teodor Zlatanov <tzz@lifelogs.com> | ||
| 6 | ;; Keywords: data | ||
| 7 | |||
| 8 | ;; This program is free software; you can redistribute it and/or modify | ||
| 9 | ;; it under the terms of the GNU General Public License as published by | ||
| 10 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 11 | ;; (at your option) any later version. | ||
| 12 | |||
| 13 | ;; This program is distributed in the hope that it will be useful, | ||
| 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 16 | ;; GNU General Public License for more details. | ||
| 17 | |||
| 18 | ;; You should have received a copy of the GNU General Public License | ||
| 19 | ;; along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| 20 | |||
| 21 | ;;; Commentary: | ||
| 22 | |||
| 23 | ;; This library provides a general-purpose EIEIO-based registry | ||
| 24 | ;; database with persistence, initialized with these fields: | ||
| 25 | |||
| 26 | ;; version: a float, 0.1 currently (don't change it) | ||
| 27 | |||
| 28 | ;; max-hard: an integer, default 5000000 | ||
| 29 | |||
| 30 | ;; max-soft: an integer, default 50000 | ||
| 31 | |||
| 32 | ;; precious: a list of symbols | ||
| 33 | |||
| 34 | ;; tracked: a list of symbols | ||
| 35 | |||
| 36 | ;; tracker: a hashtable tuned for 100 symbols to track (you should | ||
| 37 | ;; only access this with the :lookup2-function and the | ||
| 38 | ;; :lookup2+-function) | ||
| 39 | |||
| 40 | ;; data: a hashtable with default size 10K and resize threshold 2.0 | ||
| 41 | ;; (this reflects the expected usage so override it if you know better) | ||
| 42 | |||
| 43 | ;; ...plus methods to do all the work: `registry-search', | ||
| 44 | ;; `registry-lookup', `registry-lookup-secondary', | ||
| 45 | ;; `registry-lookup-secondary-value', `registry-insert', | ||
| 46 | ;; `registry-delete', `registry-prune', `registry-size' which see | ||
| 47 | |||
| 48 | ;; and with the following properties: | ||
| 49 | |||
| 50 | ;; Every piece of data has a unique ID and some general-purpose fields | ||
| 51 | ;; (F1=D1, F2=D2, F3=(a b c)...) expressed as an alist, e.g. | ||
| 52 | |||
| 53 | ;; ((F1 D1) (F2 D2) (F3 a b c)) | ||
| 54 | |||
| 55 | ;; Note that whether a field has one or many pieces of data, the data | ||
| 56 | ;; is always a list of values. | ||
| 57 | |||
| 58 | ;; The user decides which fields are "precious", F2 for example. At | ||
| 59 | ;; PRUNE TIME (when the :prune-function is called), the registry will | ||
| 60 | ;; trim any entries without the F2 field until the size is :max-soft | ||
| 61 | ;; or less. No entries with the F2 field will be removed at PRUNE | ||
| 62 | ;; TIME. | ||
| 63 | |||
| 64 | ;; When an entry is inserted, the registry will reject new entries | ||
| 65 | ;; if they bring it over the max-hard limit, even if they have the F2 | ||
| 66 | ;; field. | ||
| 67 | |||
| 68 | ;; The user decides which fields are "tracked", F1 for example. Any | ||
| 69 | ;; new entry is then indexed by all the tracked fields so it can be | ||
| 70 | ;; quickly looked up that way. The data is always a list (see example | ||
| 71 | ;; above) and each list element is indexed. | ||
| 72 | |||
| 73 | ;; Precious and tracked field names must be symbols. All other | ||
| 74 | ;; fields can be any other Emacs Lisp types. | ||
| 75 | |||
| 76 | ;;; Code: | ||
| 77 | |||
| 78 | (eval-when-compile (require 'ert)) | ||
| 79 | (eval-when-compile (require 'cl)) | ||
| 80 | (eval-and-compile | ||
| 81 | (or (ignore-errors (progn | ||
| 82 | (require 'eieio) | ||
| 83 | (require 'eieio-base))) | ||
| 84 | ;; gnus-fallback-lib/ from gnus/lisp/gnus-fallback-lib | ||
| 85 | (ignore-errors | ||
| 86 | (let ((load-path (cons (expand-file-name | ||
| 87 | "gnus-fallback-lib/eieio" | ||
| 88 | (file-name-directory (locate-library "gnus"))) | ||
| 89 | load-path))) | ||
| 90 | (require 'eieio) | ||
| 91 | (require 'eieio-base))) | ||
| 92 | (error | ||
| 93 | "eieio not found in `load-path' or gnus-fallback-lib/ directory."))) | ||
| 94 | |||
| 95 | (defclass registry-db (eieio-persistent) | ||
| 96 | ((version :initarg :version | ||
| 97 | :initform 0.1 | ||
| 98 | :type float | ||
| 99 | :custom float | ||
| 100 | :documentation "The registry version.") | ||
| 101 | (max-hard :initarg :max-hard | ||
| 102 | :initform 5000000 | ||
| 103 | :type integer | ||
| 104 | :custom integer | ||
| 105 | :documentation "Never accept more than this many elements.") | ||
| 106 | (max-soft :initarg :max-soft | ||
| 107 | :initform 50000 | ||
| 108 | :type integer | ||
| 109 | :custom integer | ||
| 110 | :documentation "Prune as much as possible to get to this size.") | ||
| 111 | (tracked :initarg :tracked | ||
| 112 | :initform nil | ||
| 113 | :type t | ||
| 114 | :documentation "The tracked (indexed) fields, a list of symbols.") | ||
| 115 | (precious :initarg :precious | ||
| 116 | :initform nil | ||
| 117 | :type t | ||
| 118 | :documentation "The precious fields, a list of symbols.") | ||
| 119 | (tracker :initarg :tracker | ||
| 120 | :type hash-table | ||
| 121 | :documentation "The field tracking hashtable.") | ||
| 122 | (data :initarg :data | ||
| 123 | :type hash-table | ||
| 124 | :documentation "The data hashtable."))) | ||
| 125 | |||
| 126 | (defmethod initialize-instance :after ((this registry-db) slots) | ||
| 127 | "Set value of data slot of THIS after initialization." | ||
| 128 | (with-slots (data tracker) this | ||
| 129 | (unless (member :data slots) | ||
| 130 | (setq data (make-hash-table :size 10000 :rehash-size 2.0 :test 'equal))) | ||
| 131 | (unless (member :tracker slots) | ||
| 132 | (setq tracker (make-hash-table :size 100 :rehash-size 2.0))))) | ||
| 133 | |||
| 134 | (defmethod registry-lookup ((db registry-db) keys) | ||
| 135 | "Search for KEYS in the registry-db THIS. | ||
| 136 | Returns a alist of the key followed by the entry in a list, not a cons cell." | ||
| 137 | (let ((data (oref db :data))) | ||
| 138 | (delq nil | ||
| 139 | (mapcar | ||
| 140 | (lambda (k) | ||
| 141 | (when (gethash k data) | ||
| 142 | (list k (gethash k data)))) | ||
| 143 | keys)))) | ||
| 144 | |||
| 145 | (defmethod registry-lookup-breaks-before-lexbind ((db registry-db) keys) | ||
| 146 | "Search for KEYS in the registry-db THIS. | ||
| 147 | Returns a alist of the key followed by the entry in a list, not a cons cell." | ||
| 148 | (let ((data (oref db :data))) | ||
| 149 | (delq nil | ||
| 150 | (loop for key in keys | ||
| 151 | when (gethash key data) | ||
| 152 | collect (list key (gethash key data)))))) | ||
| 153 | |||
| 154 | (defmethod registry-lookup-secondary ((db registry-db) tracksym | ||
| 155 | &optional create) | ||
| 156 | "Search for TRACKSYM in the registry-db THIS. | ||
| 157 | When CREATE is not nil, create the secondary index hashtable if needed." | ||
| 158 | (let ((h (gethash tracksym (oref db :tracker)))) | ||
| 159 | (if h | ||
| 160 | h | ||
| 161 | (when create | ||
| 162 | (puthash tracksym | ||
| 163 | (make-hash-table :size 800 :rehash-size 2.0 :test 'equal) | ||
| 164 | (oref db :tracker)) | ||
| 165 | (gethash tracksym (oref db :tracker)))))) | ||
| 166 | |||
| 167 | (defmethod registry-lookup-secondary-value ((db registry-db) tracksym val | ||
| 168 | &optional set) | ||
| 169 | "Search for TRACKSYM with value VAL in the registry-db THIS. | ||
| 170 | When SET is not nil, set it for VAL (use t for an empty list)." | ||
| 171 | ;; either we're asked for creation or there should be an existing index | ||
| 172 | (when (or set (registry-lookup-secondary db tracksym)) | ||
| 173 | ;; set the entry if requested, | ||
| 174 | (when set | ||
| 175 | (puthash val (if (eq t set) '() set) | ||
| 176 | (registry-lookup-secondary db tracksym t))) | ||
| 177 | (gethash val (registry-lookup-secondary db tracksym)))) | ||
| 178 | |||
| 179 | (defun registry--match (mode entry check-list) | ||
| 180 | ;; for all members | ||
| 181 | (when check-list | ||
| 182 | (let ((key (nth 0 (nth 0 check-list))) | ||
| 183 | (vals (cdr-safe (nth 0 check-list))) | ||
| 184 | found) | ||
| 185 | (while (and key vals (not found)) | ||
| 186 | (setq found (case mode | ||
| 187 | (:member | ||
| 188 | (member (car-safe vals) (cdr-safe (assoc key entry)))) | ||
| 189 | (:regex | ||
| 190 | (string-match (car vals) | ||
| 191 | (mapconcat | ||
| 192 | 'prin1-to-string | ||
| 193 | (cdr-safe (assoc key entry)) | ||
| 194 | "\0")))) | ||
| 195 | vals (cdr-safe vals))) | ||
| 196 | (or found | ||
| 197 | (registry--match mode entry (cdr-safe check-list)))))) | ||
| 198 | |||
| 199 | (defmethod registry-search ((db registry-db) &rest spec) | ||
| 200 | "Search for SPEC across the registry-db THIS. | ||
| 201 | For example calling with :member '(a 1 2) will match entry '((a 3 1)). | ||
| 202 | Calling with :all t (any non-nil value) will match all. | ||
| 203 | Calling with :regex '\(a \"h.llo\") will match entry '((a \"hullo\" \"bye\"). | ||
| 204 | The test order is to check :all first, then :member, then :regex." | ||
| 205 | (when db | ||
| 206 | (let ((all (plist-get spec :all)) | ||
| 207 | (member (plist-get spec :member)) | ||
| 208 | (regex (plist-get spec :regex))) | ||
| 209 | (loop for k being the hash-keys of (oref db :data) using (hash-values v) | ||
| 210 | when (or | ||
| 211 | ;; :all non-nil returns all | ||
| 212 | all | ||
| 213 | ;; member matching | ||
| 214 | (and member (registry--match :member v member)) | ||
| 215 | ;; regex matching | ||
| 216 | (and regex (registry--match :regex v regex))) | ||
| 217 | collect k)))) | ||
| 218 | |||
| 219 | (defmethod registry-delete ((db registry-db) keys assert &rest spec) | ||
| 220 | "Delete KEYS from the registry-db THIS. | ||
| 221 | If KEYS is nil, use SPEC to do a search. | ||
| 222 | Updates the secondary ('tracked') indices as well. | ||
| 223 | With assert non-nil, errors out if the key does not exist already." | ||
| 224 | (let* ((data (oref db :data)) | ||
| 225 | (keys (or keys | ||
| 226 | (apply 'registry-search db spec))) | ||
| 227 | (tracked (oref db :tracked))) | ||
| 228 | |||
| 229 | (dolist (key keys) | ||
| 230 | (let ((entry (gethash key data))) | ||
| 231 | (when assert | ||
| 232 | (assert entry nil | ||
| 233 | "Key %s does not exists in database" key)) | ||
| 234 | ;; clean entry from the secondary indices | ||
| 235 | (dolist (tr tracked) | ||
| 236 | ;; is this tracked symbol indexed? | ||
| 237 | (when (registry-lookup-secondary db tr) | ||
| 238 | ;; for every value in the entry under that key... | ||
| 239 | (dolist (val (cdr-safe (assq tr entry))) | ||
| 240 | (let* ((value-keys (registry-lookup-secondary-value db tr val))) | ||
| 241 | (when (member key value-keys) | ||
| 242 | ;; override the previous value | ||
| 243 | (registry-lookup-secondary-value | ||
| 244 | db tr val | ||
| 245 | ;; with the indexed keys MINUS the current key | ||
| 246 | ;; (we pass t when the list is empty) | ||
| 247 | (or (delete key value-keys) t))))))) | ||
| 248 | (remhash key data))) | ||
| 249 | keys)) | ||
| 250 | |||
| 251 | (defmethod registry-insert ((db registry-db) key entry) | ||
| 252 | "Insert ENTRY under KEY into the registry-db THIS. | ||
| 253 | Updates the secondary ('tracked') indices as well. | ||
| 254 | Errors out if the key exists already." | ||
| 255 | |||
| 256 | (assert (not (gethash key (oref db :data))) nil | ||
| 257 | "Key already exists in database") | ||
| 258 | |||
| 259 | (assert (< (registry-size db) | ||
| 260 | (oref db :max-hard)) | ||
| 261 | nil | ||
| 262 | "max-hard size limit reached") | ||
| 263 | |||
| 264 | ;; store the entry | ||
| 265 | (puthash key entry (oref db :data)) | ||
| 266 | |||
| 267 | ;; store the secondary indices | ||
| 268 | (dolist (tr (oref db :tracked)) | ||
| 269 | ;; for every value in the entry under that key... | ||
| 270 | (dolist (val (cdr-safe (assq tr entry))) | ||
| 271 | (let* ((value-keys (registry-lookup-secondary-value db tr val))) | ||
| 272 | (pushnew key value-keys :test 'equal) | ||
| 273 | (registry-lookup-secondary-value db tr val value-keys)))) | ||
| 274 | entry) | ||
| 275 | |||
| 276 | (defmethod registry-size ((db registry-db)) | ||
| 277 | "Returns the size of the registry-db object THIS. | ||
| 278 | This is the key count of the :data slot." | ||
| 279 | (hash-table-count (oref db :data))) | ||
| 280 | |||
| 281 | (defmethod registry-prune ((db registry-db)) | ||
| 282 | "Prunes the registry-db object THIS. | ||
| 283 | Removes only entries without the :precious keys." | ||
| 284 | (let* ((precious (oref db :precious)) | ||
| 285 | (precious-p (lambda (entry-key) (cdr (memq (car entry-key) precious)))) | ||
| 286 | (data (oref db :data)) | ||
| 287 | (limit (oref db :max-soft)) | ||
| 288 | (size (registry-size db)) | ||
| 289 | (candidates (loop for k being the hash-keys of data | ||
| 290 | using (hash-values v) | ||
| 291 | when (notany precious-p v) | ||
| 292 | collect k)) | ||
| 293 | (candidates-count (length candidates)) | ||
| 294 | ;; are we over max-soft? | ||
| 295 | (prune-needed (> size limit))) | ||
| 296 | |||
| 297 | ;; while we have more candidates than we need to remove... | ||
| 298 | (while (and (> candidates-count (- size limit)) candidates) | ||
| 299 | (decf candidates-count) | ||
| 300 | (setq candidates (cdr candidates))) | ||
| 301 | |||
| 302 | (registry-delete db candidates nil))) | ||
| 303 | |||
| 304 | (ert-deftest registry-instantiation-test () | ||
| 305 | (should (registry-db "Testing"))) | ||
| 306 | |||
| 307 | (ert-deftest registry-match-test () | ||
| 308 | (let ((entry '((hello "goodbye" "bye") (blank)))) | ||
| 309 | |||
| 310 | (message "Testing :regex matching") | ||
| 311 | (should (registry--match :regex entry '((hello "nye" "bye")))) | ||
| 312 | (should (registry--match :regex entry '((hello "good")))) | ||
| 313 | (should-not (registry--match :regex entry '((hello "nye")))) | ||
| 314 | (should-not (registry--match :regex entry '((hello)))) | ||
| 315 | |||
| 316 | (message "Testing :member matching") | ||
| 317 | (should (registry--match :member entry '((hello "bye")))) | ||
| 318 | (should (registry--match :member entry '((hello "goodbye")))) | ||
| 319 | (should-not (registry--match :member entry '((hello "good")))) | ||
| 320 | (should-not (registry--match :member entry '((hello "nye")))) | ||
| 321 | (should-not (registry--match :member entry '((hello))))) | ||
| 322 | (message "Done with matching testing.")) | ||
| 323 | |||
| 324 | (defun registry-make-testable-db (n &optional name file) | ||
| 325 | (let* ((db (registry-db | ||
| 326 | (or name "Testing") | ||
| 327 | :file (or file "unused") | ||
| 328 | :max-hard n | ||
| 329 | :max-soft 0 ; keep nothing not precious | ||
| 330 | :precious '(extra more-extra) | ||
| 331 | :tracked '(sender subject groups)))) | ||
| 332 | (dotimes (i n) | ||
| 333 | (registry-insert db i `((sender "me") | ||
| 334 | (subject "about you") | ||
| 335 | (more-extra) ; empty data key should be pruned | ||
| 336 | ;; first 5 entries will NOT have this extra data | ||
| 337 | ,@(when (< 5 i) (list (list 'extra "more data"))) | ||
| 338 | (groups ,(number-to-string i))))) | ||
| 339 | db)) | ||
| 340 | |||
| 341 | (ert-deftest registry-usage-test () | ||
| 342 | (let* ((n 100) | ||
| 343 | (db (registry-make-testable-db n))) | ||
| 344 | (message "size %d" n) | ||
| 345 | (should (= n (registry-size db))) | ||
| 346 | (message "max-hard test") | ||
| 347 | (should-error (registry-insert db "new" '())) | ||
| 348 | (message "Individual lookup") | ||
| 349 | (should (= 58 (caadr (registry-lookup db '(1 58 99))))) | ||
| 350 | (message "Grouped individual lookup") | ||
| 351 | (should (= 3 (length (registry-lookup db '(1 58 99))))) | ||
| 352 | (message "Individual lookup (breaks before lexbind)") | ||
| 353 | (should (= 58 | ||
| 354 | (caadr (registry-lookup-breaks-before-lexbind db '(1 58 99))))) | ||
| 355 | (message "Grouped individual lookup (breaks before lexbind)") | ||
| 356 | (should (= 3 | ||
| 357 | (length (registry-lookup-breaks-before-lexbind db '(1 58 99))))) | ||
| 358 | (message "Search") | ||
| 359 | (should (= n (length (registry-search db :all t)))) | ||
| 360 | (should (= n (length (registry-search db :member '((sender "me")))))) | ||
| 361 | (message "Secondary index search") | ||
| 362 | (should (= n (length (registry-lookup-secondary-value db 'sender "me")))) | ||
| 363 | (should (equal '(74) (registry-lookup-secondary-value db 'groups "74"))) | ||
| 364 | (message "Delete") | ||
| 365 | (should (registry-delete db '(1) t)) | ||
| 366 | (decf n) | ||
| 367 | (message "Search after delete") | ||
| 368 | (should (= n (length (registry-search db :all t)))) | ||
| 369 | (message "Secondary search after delete") | ||
| 370 | (should (= n (length (registry-lookup-secondary-value db 'sender "me")))) | ||
| 371 | (message "Pruning") | ||
| 372 | (let* ((tokeep (registry-search db :member '((extra "more data")))) | ||
| 373 | (count (- n (length tokeep))) | ||
| 374 | (pruned (registry-prune db)) | ||
| 375 | (prune-count (length pruned))) | ||
| 376 | (message "Expecting to prune %d entries and pruned %d" | ||
| 377 | count prune-count) | ||
| 378 | (should (and (= count 5) | ||
| 379 | (= count prune-count)))) | ||
| 380 | (message "Done with usage testing."))) | ||
| 381 | |||
| 382 | (ert-deftest registry-persistence-test () | ||
| 383 | (let* ((n 100) | ||
| 384 | (tempfile (make-temp-file "registry-persistence-")) | ||
| 385 | (name "persistence tester") | ||
| 386 | (db (registry-make-testable-db n name tempfile)) | ||
| 387 | size back) | ||
| 388 | (message "Saving to %s" tempfile) | ||
| 389 | (eieio-persistent-save db) | ||
| 390 | (setq size (nth 7 (file-attributes tempfile))) | ||
| 391 | (message "Saved to %s: size %d" tempfile size) | ||
| 392 | (should (< 0 size)) | ||
| 393 | (with-temp-buffer | ||
| 394 | (insert-file-contents-literally tempfile) | ||
| 395 | (should (looking-at (concat ";; Object " | ||
| 396 | name | ||
| 397 | "\n;; EIEIO PERSISTENT OBJECT")))) | ||
| 398 | (message "Reading object back") | ||
| 399 | (setq back (eieio-persistent-read tempfile)) | ||
| 400 | (should back) | ||
| 401 | (message "Read object back: %d keys, expected %d==%d" | ||
| 402 | (registry-size back) n (registry-size db)) | ||
| 403 | (should (= (registry-size back) n)) | ||
| 404 | (should (= (registry-size back) (registry-size db))) | ||
| 405 | (delete-file tempfile)) | ||
| 406 | (message "Done with persistence testing.")) | ||
| 407 | |||
| 408 | (provide 'registry) | ||
| 409 | ;;; registry.el ends here | ||