Repository: TileDB-Inc/TileDB-Py

Issue:
likely race condition in DenseArray.__new__

Test program below exposes (what I believe is a) race in class handling in DenseArray.  The test program is completely artificial - it just spins up 5 threads, each of which open an S3 object and ask its shape.   Without a sleep to sequence their execution, it fails inside the DenseArray.__new__ method.

Run with the sleep statement in worker(), I see:
```
python cctest.py
starting... 0
starting... 1
working... 0
ending... 0 (1305994, 972)
starting... 2
working... 1
ending... 1 (1305994, 972)
starting... 3
working... 2
ending... 2 (1305994, 972)
starting... 4
working... 3
ending... 3 (1305994, 972)
working... 4
ending... 4 (1305994, 972)
all done [(1305994, 972), (1305994, 972), (1305994, 972), (1305994, 972), (1305994, 972)]
```

Run with the sleep in worker removed/commented out:
```
$ python cctest.py
starting... 0
starting... 1
starting... 2
working... 0
ending... 0 (1305994, 972)
starting... 3
working... 2
ending... 2 (1305994, 972)
starting... 4
working... 3
ending... 3 (1305994, 972)
working... 4
ending... 4 (1305994, 972)
Traceback (most recent call last):
  File "cctest.py", line 33, in <module>
    main()
  File "cctest.py", line 27, in main
    res = [f.result() for f in concurrent.futures.as_completed(futures)]
  File "cctest.py", line 27, in <listcomp>
    res = [f.result() for f in concurrent.futures.as_completed(futures)]
  File "/usr/lib64/python3.7/concurrent/futures/_base.py", line 428, in result
    return self.__get_result()
  File "/usr/lib64/python3.7/concurrent/futures/_base.py", line 384, in __get_result
    raise self._exception
  File "/usr/lib64/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "cctest.py", line 11, in worker
    with tiledb.DenseArray(uri) as A:
  File "/home/ec2-user/venv/lib64/python3.7/site-packages/tiledb/array.py", line 10, in __new__
    DenseArray.__bases__ = DenseArray.__bases__ + (cloudarray.CloudArray,)
TypeError: duplicate base class CloudArray
```

Test case:
```
import tiledb
import time
import numpy as np
import concurrent.futures


def worker(n, uri):
    print('starting...', n)
    # time.sleep(n)

    with tiledb.DenseArray(uri) as A:
        print('working...', n)
        res = A.shape

    print('ending...', n, res)
    return res


def main():
    executor_cls = concurrent.futures.ThreadPoolExecutor

    uri = "s3://tiledb-eval/10x_mouse_13MM_processed-Xr3zstd.cxg/X"

    with executor_cls(max_workers=2) as executor:
        futures = [executor.submit(worker, n, uri) for n in range(0, 5)]
        res = [f.result() for f in concurrent.futures.as_completed(futures)]

    print('all done', res)


if __name__ == '__main__':
    main()
```


Discussion:
This is probably https://bugs.python.org/issue38884 -- apparently `import` is not thread-safe, despite both the GIL and the import lock. Will work around.

Resolving patch:
diff --git a/tiledb/array.py b/tiledb/array.py
--- a/tiledb/array.py
+++ b/tiledb/array.py
@@ -1,17 +1,29 @@
 from .libtiledb import DenseArrayImpl, SparseArrayImpl
 
+# Extensible (pure Python) array class definitions inheriting from the
+# Cython implemention. The cloudarray mix-in adds optional functionality
+# for registering arrays and executing functions on the
+
+# NOTE: the mixin import must be inside the __new__ initializer because it
+#       needs to be deferred. tiledb.cloud is not yet known to the importer
+#       when this code is imported.
+#       TODO: might be possible to work-around/simplify by using
+#       import meta-hooks instead.
+
 class DenseArray(DenseArrayImpl):
     _mixin_init = False
 
     def __new__(cls, *args, **kwargs):
         if not cls._mixin_init:
+            # must set before importing, because import is not thread-safe
+            #   https://github.com/TileDB-Inc/TileDB-Py/issues/244
+            cls._mixin_init = True
             try:
                 from tiledb.cloud import cloudarray
                 DenseArray.__bases__ = DenseArray.__bases__ + (cloudarray.CloudArray,)
+                DenseArray.__doc__ = DenseArrayImpl.__doc__
             except ImportError:
                 pass
-            finally:
-                cls._mixin_init = True
 
         obj = super(DenseArray, cls).__new__(cls, *args, **kwargs)
         return obj
@@ -21,13 +33,13 @@ class SparseArray(SparseArrayImpl):
 
     def __new__(cls, *args, **kwargs):
         if not cls._mixin_init:
+            cls._mixin_init = True
             try:
                 from tiledb.cloud import cloudarray
                 SparseArray.__bases__ = SparseArray.__bases__ + (cloudarray.CloudArray,)
+                SparseArray.__doc__ = DenseArrayImpl.__doc__
             except ImportError:
                 pass
-            finally:
-                cls._mixin_init = True
 
         obj = super(SparseArray, cls).__new__(cls, *args, **kwargs)
         return obj


Test patch: