Skip to content

synchronized hash tables with weak keys may have duplicate entries

When using synchronized hash tables with weak keys (either :weakness :key or :weakness :key-and-value) we sometimes get duplicated entries, so we can't recover the value reliably. The test case is awfully big, but that's what I have for now. I'll try to minimize it later.

(in-package "CL-USER")

(eval-when (:compile-toplevel :execute :load-toplevel)

  (unless (find-package "BORDEAUX-THREADS")
    #+quicklisp (ql:quickload "bordeaux-threads")
    #-quicklisp (error "Please load \"bordeaux-threads\"."))

  (unless (find-package "TRIVIAL-GARBAGE")
    #+quicklisp (ql:quickload "bordeaux-threads")
    #-quicklisp (error "Please load \"trivial-garbage\".")))

(in-package "CL-USER")



(defun make-dynamic-variable-using-class (class &rest initargs)
  (apply #'make-instance class initargs))

(defparameter *default-dynamic-variable-class*
  'surrogate-dynamic-variable)

(defun make-dynamic-variable (&rest initargs)
  (apply #'make-dynamic-variable-using-class *default-dynamic-variable-class*
         initargs))


(defmacro dlet (bindings &body body)
  (flet ((pred (binding)
           (and (listp binding) (= 2 (length binding)))))
    (unless (every #'pred bindings)
      (error "DLET: bindings must be lists of two values.~%~
              Invalid bindings:~%~{ ~s~%~}" (remove-if #'pred bindings))))
  (loop for (var val) in bindings
        collect var into vars
        collect val into vals
        finally (return `(dynamic-variable-progv (list ,@vars) (list ,@vals)
                           ,@body))))

(defmacro dset (&rest pairs)
  `(setf ,@(loop for (var val) on pairs by #'cddr
                 collect `(dref ,var)
                 collect val)))

(defmacro dref (variable)
  `(dynamic-variable-value ,variable))


(defun call-with-dynamic-variable-progv (cont vars vals)
  (flet ((thunk ()
           (if vals
               (call-with-dynamic-variable cont (car vars) (car vals))
               (call-with-dynamic-variable cont (car vars)))))
    (if vars
        (call-with-dynamic-variable-progv #'thunk (cdr vars) (cdr vals))
        (funcall cont))))

(defmacro dynamic-variable-progv (vars vals &body body)
  (let ((cont (gensym)))
    `(flet ((,cont () ,@body))
       (call-with-dynamic-variable-progv (function ,cont) ,vars ,vals))))



(defvar +fake-unbound+ 'unbound)
(defvar +cell-unbound+ '(no-binding))

;;; CCL can't into :KEY-AND-VALUE.
(defclass surrogate-dynamic-variable ()
  ((tls-table
    :initform (make-hash-table :synchronized t
                               :weakness :key)
    :reader dynamic-variable-tls-table)
   (top-value
    :initform +fake-unbound+
    :accessor dynamic-variable-top-value)))

(defun %dynamic-variable-bindings (dvar)
  (let ((process (bt:current-thread))
        (tls-table (dynamic-variable-tls-table dvar)))
    (or (gethash process tls-table)
        (setf (gethash process tls-table) +cell-unbound+))))

(defun (setf %dynamic-variable-bindings) (value dvar)
  (let ((process (bt:current-thread))
        (tls-table (dynamic-variable-tls-table dvar)))
    (when (< (length value) 1)
      (error "booya"))
    (setf (gethash process tls-table) value)))

(defun dynamic-variable-value (dvar)
  (let ((tls-binds (%dynamic-variable-bindings dvar)))
    (if (eq tls-binds +cell-unbound+)
        (dynamic-variable-top-value dvar)
        (car tls-binds))))

(defun (setf dynamic-variable-value) (value dvar)
  (let ((tls-binds (%dynamic-variable-bindings dvar)))
    (if (eq tls-binds +cell-unbound+)
        (setf (dynamic-variable-top-value dvar) value)
        (setf (car tls-binds) value))))

(defvar *badhash* nil)
(defun call-with-dynamic-variable (cont dvar &optional (val +fake-unbound+))
  (push val (%dynamic-variable-bindings dvar))
  (assert (>= (length (%dynamic-variable-bindings dvar)) 2) nil "firt")
  (unwind-protect (funcall cont)
    (let ((bindings (%dynamic-variable-bindings dvar)))
      (unless (>= (length bindings) 2)
        (format t "----------------------------------~%")
        (format t "~a? ~a~%" (bt:current-thread) bindings)
        (setf *badhash* (dynamic-variable-tls-table dvar))
        (maphash (lambda (k v)
                   (if (eq k (bt:current-thread))
                       (format t "~a: ~a <---~%" k v)
                       (format t "~a: ~a~%" k v)))
                 (dynamic-variable-tls-table dvar))
        (error "plops")))
    (pop (%dynamic-variable-bindings dvar))))


#+ (or)
(defun call-with-dynamic-variable (cont dvar &optional (val +fake-unbound+))
  (push val (%dynamic-variable-bindings dvar))
  (assert (>= (length (%dynamic-variable-bindings dvar)) 2) nil "firt")
  (unwind-protect (funcall cont)
    (assert (>= (length (%dynamic-variable-bindings dvar)) 2) nil "secd")
    (pop (%dynamic-variable-bindings dvar))))

;;; This is a metaclass that allows defining dynamic slots that are bound with
;;; the operator SLOT-DLET.

;;; Slot definitions
;;; There is a considerable boilerplate involving customizing slots.
;;;
;;; - direct slot definition: local to a single defclass form
;;;
;;; - effective slot definition: combination of all direct slots with the same
;;;   name in the class and its superclasses
;;;
(defclass dynamic-direct-slot (mop:standard-direct-slot-definition)
  ((dynamic :initform nil :initarg :dynamic :reader dynamic-slot-p)))

;;; DYNAMIC-EFFECTIVE-SLOT is implemented to return as slot-value values of the
;;; dynamic variable that is stored with the instance.
;;;
;;; It would be nice if we could specify :ALLOCATION :DYNAMIC for the slot, but
;;; then STANDARD-INSTANCE-ACCESS would go belly up. We could make a clever
;;; workaround, but who cares?
(defclass dynamic-effective-slot (mop:standard-effective-slot-definition)
  ())

(defmethod mop:slot-value-using-class
    ((class standard-class)
     object
     (slotd dynamic-effective-slot))
  (dref (slot-dvar object slotd)))

(defmethod (setf mop:slot-value-using-class)
    (new-value
     (class standard-class)
     object
     (slotd dynamic-effective-slot))
  (dset (slot-dvar object slotd) new-value))

(defmethod mop:slot-boundp-using-class
  ((class standard-class)
   object
   (slotd dynamic-effective-slot))
  (dynamic-variable-bound-p (slot-dvar object slotd)))

(defmethod mop:slot-makunbound-using-class
  ((class standard-class)
   object
   (slotd dynamic-effective-slot))
  (dynamic-variable-makunbound (slot-dvar object slotd)))


;;; The metaclass CLASS-WITH-DYNAMIC-SLOTS specifies alternative effective slot
;;; definitions for slots with an initarg :dynamic.
(defclass class-with-dynamic-slots (standard-class) ())

;;; Class with dynamic slots may be subclasses of the standard class.
(defmethod mop:validate-superclass ((class class-with-dynamic-slots)
                                    (super standard-class))
  t)

;;; When allocating the instance we initialize all slots to a fresh symbol that
;;; represents the dynamic variable.
(defmethod allocate-instance ((class class-with-dynamic-slots) &rest initargs)
  (declare (ignore initargs))
  (let ((object (call-next-method)))
    (loop for slotd in (mop:class-slots class)
          when (typep slotd 'dynamic-effective-slot) do
            (setf (mop:standard-instance-access
                   object
                   (mop:slot-definition-location slotd))
                  (make-dynamic-variable)))
    object))

;;; To improve potential composability of CLASS-WITH-DYNAMIC-SLOTS with other
;;; metaclasses we treat specially only slots that has :DYNAMIC in initargs,
;;; otherwise we call the next method.
(defmethod mop:direct-slot-definition-class
    ((class class-with-dynamic-slots) &rest initargs)
  (loop for (key) on initargs by #'cddr
        when (eq key :dynamic)
          do (return-from mop:direct-slot-definition-class
               (find-class 'dynamic-direct-slot)))
  (call-next-method))

;;; The metaobject protocol did not specify an elegant way to communicate
;;; between the direct slot definition and the effective slot definition.
;;; Luckily we have dynamic bindings! :-)
(defvar *kludge/mop-deficiency/dynamic-slot-p* nil)
(defmethod mop:compute-effective-slot-definition
    ((class class-with-dynamic-slots)
     name
     direct-slotds)
  (declare (ignore name))
  (let ((latest-slotd (first direct-slotds)))
    (if (typep latest-slotd 'dynamic-direct-slot)
        (let ((*kludge/mop-deficiency/dynamic-slot-p*
                (dynamic-slot-p latest-slotd)))
          (call-next-method))
        (call-next-method))))

(defmethod mop:effective-slot-definition-class
    ((class class-with-dynamic-slots) &rest initargs)
  (declare (ignore initargs))
  (if *kludge/mop-deficiency/dynamic-slot-p*
      (find-class 'dynamic-effective-slot)
      (call-next-method)))

;;; Accessing and binding symbols behind the slot. We don't use SLOT-VALUE,
;;; because it will return the _value_ of the dynamic variable, and not the
;;; variable itself.
(defun slot-dvar (object slotd)
  (check-type slotd dynamic-effective-slot)
  (mop:standard-instance-access
   object (mop:slot-definition-location slotd)))

(defun slot-dvar* (object slot-name)
  (let* ((class (class-of object))
         (slotd (find slot-name (mop:class-slots class)
                      :key #'mop:slot-definition-name)))
    (slot-dvar object slotd)))

(defmacro slot-dlet (bindings &body body)
  `(dlet ,(loop for ((object slot-name) val) in bindings
                collect `((slot-dvar* ,object ,slot-name) ,val))
     ,@body))


(defclass dynamic-let.test-class ()
  ((slot1 :initarg :slot1 :dynamic nil :accessor slot1)
   (slot2 :initarg :slot2 :dynamic t   :accessor slot2)
   (slot3 :initarg :slot3              :accessor slot3))
  (:metaclass class-with-dynamic-slots))

(defmacro is (&body body) `(assert ,@body))

(defparameter *dynamic-let.test-instance-1*
  (make-instance 'dynamic-let.test-class
                 :slot1 :a :slot2 :b :slot3 :c))

(defun my-test ()
  (let ((o1 *dynamic-let.test-instance-1*)
       (fail nil))
   (is (eq (slot2 o1) :b))
   (flet ((make-runner (values)
            (lambda ()
              (slot-dlet (((o1 'slot2) :start))
                (let ((value (slot2 o1)))
                  (unless (eq value :start)
                    (setf fail value)))
                (loop repeat 1024 do
                  (setf (slot2 o1) (elt values (random (length values))))
                  (let ((value (slot2 o1)))
                    (unless (member value values)
                      (setf fail value))))))))
     (let ((threads (loop for i from 0 below 64
                          for v = (list (make-symbol (format nil "A~d" i))
                                        (make-symbol (format nil "B~d" i))
                                        (make-symbol (format nil "C~d" i)))
                          collect (bt:make-thread (make-runner v)))))
       (map nil #'bt:join-thread threads)
       (is (eq (slot2 o1) :b))
       (is (null fail))))))

Finally

(dotimes (v 64) (my-test))
;; expected NIL

;; observed
Long listing of key-val pairs (from maphash):
...
NIL: (NO-BINDING)
NIL: (NO-BINDING)
#<process Anonymous thread 0x7f57a166b0c0>: (NO-BINDING)
NIL: (NO-BINDING)
#<process repl-thread 0x7f57b44803c0>: (NO-BINDING)
#<process Anonymous thread 0x7f57a9066d80>: (NO-BINDING)
#<process Anonymous thread 0x7f57a166bb40>: (A2 NO-BINDING) <---
#<process Anonymous thread 0x7f57a166bb40>: (NO-BINDING) <---
#<process Anonymous thread 0x7f57a157e6c0>: (NO-BINDING)
NIL: (NO-BINDING)
NIL: (NO-BINDING)
...

Note that we have two entries with the same unique key. That leads to a corruption of "simulated" bindings in this program.

Note that this issue prevails after fixing the recent issue with rw locks (in other words it is independent).