shareddata 6.83.12__tar.gz → 6.83.17__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {shareddata-6.83.12 → shareddata-6.83.17}/MANIFEST.in +0 -1
- {shareddata-6.83.12/src/shareddata.egg-info → shareddata-6.83.17}/PKG-INFO +3 -2
- {shareddata-6.83.12 → shareddata-6.83.17}/setup.py +2 -2
- shareddata-6.83.17/src/SharedData/API/listcache.py +91 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/routes/collections.py +14 -6
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/routes/tables.py +74 -39
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/routes/timeseries.py +18 -15
- shareddata-6.83.17/src/SharedData/Defaults.py +313 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/MongoDBClient.py +2 -2
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Logger.py +11 -7
- shareddata-6.83.17/src/SharedData/Routines/ArchiveOldCollections.py +254 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Routines/Schedule.py +3 -3
- shareddata-6.83.17/src/SharedData/Routines/UpdateActiveJobs.py +75 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Routines/Worker.py +11 -10
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Routines/WorkerPool.py +164 -147
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/SharedData.py +54 -9
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/SharedNumpy.py +6 -6
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Table.py +138 -43
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TableIndex.py +32 -28
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17/src/shareddata.egg-info}/PKG-INFO +3 -2
- {shareddata-6.83.12 → shareddata-6.83.17}/src/shareddata.egg-info/SOURCES.txt +13 -1
- shareddata-6.83.17/tests/test_api_table_production_flows.py +414 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_bson_last_pos_reuse.py +5 -5
- shareddata-6.83.17/tests/test_listcache.py +94 -0
- shareddata-6.83.17/tests/test_logger_thread_safety.py +47 -0
- shareddata-6.83.17/tests/test_mount_unmount_detection.py +93 -0
- shareddata-6.83.17/tests/test_schema_acquire_timeout.py +47 -0
- shareddata-6.83.17/tests/test_table_auto_grow_all_pkeys.py +98 -0
- shareddata-6.83.17/tests/test_workerpool_jobs_status.py +283 -0
- shareddata-6.83.17/tests/test_workerpool_mutex_release.py +55 -0
- shareddata-6.83.17/tests/test_workerpool_post_commands.py +146 -0
- shareddata-6.83.12/src/SharedData/Defaults.py +0 -102
- {shareddata-6.83.12 → shareddata-6.83.17}/LICENSE +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/README.md +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/pyproject.toml +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/setup.cfg +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/__init__.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/auth.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/routes/cache.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Database.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/ClientAPI.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/SaveTables.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Routines/WorkerLib.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TableDisk.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TableIndexJitFunctions.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TableIndexJitGenerate.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Users.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/Utils.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/SharedData/__init__.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/src/shareddata.egg-info/top_level.txt +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_api_collection.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_api_collection_loopback.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_api_table.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_api_table_schemaless.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_api_table_schemaless_extend.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_cache_redis.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_extend_rt.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_loc.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_metadata.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_read_write_tail.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_stream_loopback_async.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_timeseries.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_timeseries_api.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_d1.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_m1.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_m15.py +0 -0
- {shareddata-6.83.12 → shareddata-6.83.17}/tests/test_upsert_unordered_positions_m1.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shareddata
|
|
3
|
-
Version: 6.83.
|
|
3
|
+
Version: 6.83.17
|
|
4
4
|
Summary: Memory Mapped / Shared Memory Database with S3 repository
|
|
5
|
+
License: MIT
|
|
5
6
|
Classifier: Programming Language :: Python :: 3
|
|
6
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
7
7
|
Classifier: Operating System :: OS Independent
|
|
8
8
|
Requires-Python: >=3.9, <3.13
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
@@ -39,6 +39,7 @@ Requires-Dist: redis==6.2.0
|
|
|
39
39
|
Dynamic: classifier
|
|
40
40
|
Dynamic: description
|
|
41
41
|
Dynamic: description-content-type
|
|
42
|
+
Dynamic: license
|
|
42
43
|
Dynamic: license-file
|
|
43
44
|
Dynamic: requires-dist
|
|
44
45
|
Dynamic: requires-python
|
|
@@ -34,13 +34,13 @@ install_requires = [
|
|
|
34
34
|
|
|
35
35
|
setup(
|
|
36
36
|
name='shareddata',
|
|
37
|
-
version='6.83.
|
|
37
|
+
version='6.83.17',
|
|
38
38
|
description='Memory Mapped / Shared Memory Database with S3 repository',
|
|
39
|
+
license='MIT',
|
|
39
40
|
long_description=open('README.md').read(),
|
|
40
41
|
long_description_content_type='text/markdown',
|
|
41
42
|
classifiers=[
|
|
42
43
|
'Programming Language :: Python :: 3',
|
|
43
|
-
'License :: OSI Approved :: MIT License',
|
|
44
44
|
'Operating System :: OS Independent',
|
|
45
45
|
],
|
|
46
46
|
packages=find_packages(where='src'),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TTL + single-flight cache for the expensive listing endpoints.
|
|
3
|
+
|
|
4
|
+
``/api/tables``, ``/api/timeseries`` and ``/api/collections`` enumerate every
|
|
5
|
+
partition on S3 (``list_remote``) and on local disk (``list_local``). With an
|
|
6
|
+
empty ``keyword`` this scans the entire data lake and can take well over a
|
|
7
|
+
minute. When several callers (the Dashboard polling + workers) hit the route at
|
|
8
|
+
once, each one launches an independent full scan, the responses materialize in
|
|
9
|
+
memory simultaneously, and the box sawtooths into swap while every request
|
|
10
|
+
exceeds the upstream gateway timeout and returns 502/504.
|
|
11
|
+
|
|
12
|
+
This cache fixes the dominant failure mode: it memoizes each listing for a short
|
|
13
|
+
TTL and, crucially, runs the producer under a per-key lock (single-flight) so at
|
|
14
|
+
most one scan executes per key at a time. Concurrent callers block on the lock
|
|
15
|
+
and then receive the freshly cached result instead of each starting their own
|
|
16
|
+
scan. Steady-state polling becomes instant; the storm of overlapping full scans
|
|
17
|
+
disappears.
|
|
18
|
+
|
|
19
|
+
Note: the very first (cold) call for a key still pays the full scan cost. That
|
|
20
|
+
is inherent to a no-keyword full enumeration; pair this with a higher gateway
|
|
21
|
+
read-timeout on the listing routes if cold calls must also stay under the proxy
|
|
22
|
+
limit.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Entry:
|
|
30
|
+
__slots__ = ("value", "ts", "lock")
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self.value = None
|
|
34
|
+
self.ts = 0.0
|
|
35
|
+
self.lock = threading.Lock()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TTLSingleFlightCache:
|
|
39
|
+
"""Thread-safe TTL cache that collapses concurrent misses into one producer call."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, ttl=60.0):
|
|
42
|
+
self.ttl = float(ttl)
|
|
43
|
+
self._entries = {}
|
|
44
|
+
self._guard = threading.Lock()
|
|
45
|
+
|
|
46
|
+
def _entry(self, key):
|
|
47
|
+
# Fast path under a short global lock just to fetch/create the per-key entry.
|
|
48
|
+
with self._guard:
|
|
49
|
+
entry = self._entries.get(key)
|
|
50
|
+
if entry is None:
|
|
51
|
+
entry = _Entry()
|
|
52
|
+
self._entries[key] = entry
|
|
53
|
+
return entry
|
|
54
|
+
|
|
55
|
+
def get(self, key, producer, ttl=None):
|
|
56
|
+
"""
|
|
57
|
+
Return the cached value for ``key`` if fresh, otherwise call ``producer()``.
|
|
58
|
+
|
|
59
|
+
Only one thread runs ``producer`` per key at a time; others wait on the
|
|
60
|
+
per-key lock and then receive the value the winner just stored.
|
|
61
|
+
"""
|
|
62
|
+
ttl = self.ttl if ttl is None else float(ttl)
|
|
63
|
+
entry = self._entry(key)
|
|
64
|
+
|
|
65
|
+
# Fresh hit, no locking needed.
|
|
66
|
+
if entry.value is not None and (time.time() - entry.ts) < ttl:
|
|
67
|
+
return entry.value
|
|
68
|
+
|
|
69
|
+
# Stale or empty -> single-flight refresh.
|
|
70
|
+
with entry.lock:
|
|
71
|
+
# Another thread may have refreshed while we waited on the lock.
|
|
72
|
+
if entry.value is not None and (time.time() - entry.ts) < ttl:
|
|
73
|
+
return entry.value
|
|
74
|
+
value = producer()
|
|
75
|
+
entry.value = value
|
|
76
|
+
entry.ts = time.time()
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
def invalidate(self, key=None):
|
|
80
|
+
"""Drop one key (or the whole cache when ``key`` is None)."""
|
|
81
|
+
with self._guard:
|
|
82
|
+
if key is None:
|
|
83
|
+
self._entries.clear()
|
|
84
|
+
else:
|
|
85
|
+
self._entries.pop(key, None)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Module-level singleton shared by the listing routes.
|
|
89
|
+
# 60s TTL: listing metadata changes slowly, and a minute of staleness is
|
|
90
|
+
# acceptable for a directory view while it eliminates the scan pile-up.
|
|
91
|
+
LIST_CACHE = TTLSingleFlightCache(ttl=60.0)
|
|
@@ -23,6 +23,7 @@ from SharedData.API.constants import (
|
|
|
23
23
|
DEFAULT_PAGE_SIZE,
|
|
24
24
|
ERROR_SLEEP_SECONDS,
|
|
25
25
|
)
|
|
26
|
+
from SharedData.API.listcache import LIST_CACHE
|
|
26
27
|
from SharedData.API.utils import (
|
|
27
28
|
apply_subfolder,
|
|
28
29
|
make_empty_response,
|
|
@@ -99,13 +100,20 @@ def init_collections_routes(shdata, authenticate_fn):
|
|
|
99
100
|
return jsonify({'error': 'unauthorized'}), 401
|
|
100
101
|
keyword = request.args.get('keyword', '')
|
|
101
102
|
user = request.args.get('user', DEFAULT_USER)
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
|
|
104
|
+
def _produce():
|
|
105
|
+
collections = shdata.list_collections(keyword=keyword, user=user)
|
|
106
|
+
if collections.empty:
|
|
107
|
+
return b'' # sentinel -> 204
|
|
108
|
+
collections_recs = collections.reset_index().to_dict('records')
|
|
109
|
+
collections_recs = CollectionMongoDB.serialize(collections_recs, iso_dates=True)
|
|
110
|
+
return json.dumps(collections_recs).encode('utf-8')
|
|
111
|
+
|
|
112
|
+
# Single-flight + TTL cache: see listcache.py.
|
|
113
|
+
response_data = LIST_CACHE.get(('collections', user, keyword), _produce)
|
|
114
|
+
if not response_data:
|
|
104
115
|
return Response(status=204)
|
|
105
|
-
|
|
106
|
-
collections = CollectionMongoDB.serialize(collections, iso_dates=True)
|
|
107
|
-
response_data = json.dumps(collections).encode('utf-8')
|
|
108
|
-
response = Response(response_data, mimetype='application/json')
|
|
116
|
+
response = Response(response_data, mimetype='application/json')
|
|
109
117
|
return response
|
|
110
118
|
except Exception as e:
|
|
111
119
|
response = Response(status=500)
|
|
@@ -21,6 +21,7 @@ from SharedData.API.constants import (
|
|
|
21
21
|
ERROR_SLEEP_SECONDS,
|
|
22
22
|
MAX_RESPONSE_SIZE_BYTES,
|
|
23
23
|
)
|
|
24
|
+
from SharedData.API.listcache import LIST_CACHE
|
|
24
25
|
from SharedData.API.utils import (
|
|
25
26
|
apply_subfolder,
|
|
26
27
|
make_empty_response,
|
|
@@ -56,12 +57,19 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
56
57
|
keyword = request.args.get('keyword', '')
|
|
57
58
|
user = request.args.get('user', DEFAULT_USER)
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
def _produce():
|
|
61
|
+
tables = shdata.list_tables(keyword=keyword, user=user)
|
|
62
|
+
if tables.empty:
|
|
63
|
+
return b'' # sentinel -> 204
|
|
64
|
+
tables_recs = tables.reset_index().to_dict('records')
|
|
65
|
+
tables_recs = CollectionMongoDB.serialize(tables_recs, iso_dates=True)
|
|
66
|
+
return json.dumps(tables_recs).encode('utf-8')
|
|
67
|
+
|
|
68
|
+
# Single-flight + TTL cache: collapses concurrent/repeat full scans so
|
|
69
|
+
# they no longer pile up past the gateway timeout (see listcache.py).
|
|
70
|
+
response_data = LIST_CACHE.get(('tables', user, keyword), _produce)
|
|
71
|
+
if not response_data:
|
|
61
72
|
return Response(status=204)
|
|
62
|
-
tables = tables.reset_index().to_dict('records')
|
|
63
|
-
tables = CollectionMongoDB.serialize(tables, iso_dates=True)
|
|
64
|
-
response_data = json.dumps(tables).encode('utf-8')
|
|
65
73
|
response = Response(response_data, mimetype='application/json')
|
|
66
74
|
response.headers['Content-Length'] = len(response_data)
|
|
67
75
|
return response
|
|
@@ -190,14 +198,36 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
190
198
|
loc = np.array(loc)
|
|
191
199
|
|
|
192
200
|
# cycle query keys
|
|
193
|
-
if query.keys() is not None:
|
|
194
|
-
for key in query.keys():
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
if query.keys() is not None:
|
|
202
|
+
for key in query.keys():
|
|
203
|
+
# Take the column view FIRST, then index it.
|
|
204
|
+
#
|
|
205
|
+
# `tbl[key]` is zero-copy (SharedNumpy.__getitem__ slices to
|
|
206
|
+
# count, and a field name on an ndarray is a view), whereas
|
|
207
|
+
# `tbl[loc]` is fancy indexing -- a copy of every selected row
|
|
208
|
+
# at the FULL record width, done once per query key and applied
|
|
209
|
+
# BEFORE the 20 MiB response cap, so it was unbounded.
|
|
210
|
+
#
|
|
211
|
+
# Measured on a 208-byte record: old peak was exactly
|
|
212
|
+
# 217.0 B/row at 100k/200k/400k/800k rows and FLAT across a
|
|
213
|
+
# 2000x selectivity sweep (it copies the whole table either
|
|
214
|
+
# way); new peak is ~1 B/row.
|
|
215
|
+
#
|
|
216
|
+
# `tbl[:]` (not `tbl[key]`) is deliberate: SharedNumpy.__getitem__
|
|
217
|
+
# re-reads tbl.count on every access, so slicing the whole table
|
|
218
|
+
# here observes the count at COMPARISON time, exactly as the old
|
|
219
|
+
# `tbl[loc]` did. Caching a column view from an earlier count
|
|
220
|
+
# would diverge if a concurrent writer shrank the table -- e.g.
|
|
221
|
+
# TableIndex.create_index() lowers hdr['count'] when it drops
|
|
222
|
+
# invalid/duplicate rows -- letting this succeed where the old
|
|
223
|
+
# code raised IndexError (and vice versa on growth).
|
|
224
|
+
col = tbl[:][key]
|
|
225
|
+
if pd.api.types.is_string_dtype(col):
|
|
226
|
+
idx = col[loc] == query[key].encode()
|
|
227
|
+
elif pd.api.types.is_datetime64_any_dtype(col):
|
|
228
|
+
idx = col[loc] == pd.Timestamp(query[key])
|
|
229
|
+
else:
|
|
230
|
+
idx = col[loc] == query[key]
|
|
201
231
|
loc = loc[idx]
|
|
202
232
|
|
|
203
233
|
if len(loc) == 0:
|
|
@@ -206,28 +236,31 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
206
236
|
# filter columns
|
|
207
237
|
pkey = DATABASE_PKEYS[database]
|
|
208
238
|
columns = request.args.get('columns')
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
# NOTE: `columns` on a SCHEMALESS table falls through to the plain
|
|
240
|
+
# pagination branch below. It used to hit `else: pass`, which left
|
|
241
|
+
# startpage/endpage/content_pages/recs2send unbound -> UnboundLocalError
|
|
242
|
+
# -> 500 -> 1s in-handler sleep -> 3 immediate client retries. The
|
|
243
|
+
# projection for schemaless tables is applied to the decoded dict_list
|
|
244
|
+
# further down instead.
|
|
245
|
+
if columns and not tbl.is_schemaless:
|
|
246
|
+
columns = columns.split(',')
|
|
247
|
+
columns = np.array([c for c in columns if c not in pkey])
|
|
248
|
+
columns = pkey + list(np.unique(columns))
|
|
249
|
+
names = columns
|
|
250
|
+
formats = [tbl.dtype.fields[name][0].str for name in names]
|
|
251
|
+
dtype = np.dtype(list(zip(names, formats)))
|
|
252
|
+
# Apply pagination
|
|
253
|
+
maxrows = int(np.floor(MAX_RESPONSE_SIZE_BYTES / dtype.itemsize))
|
|
254
|
+
maxrows = min(maxrows, len(loc))
|
|
255
|
+
if (per_page > maxrows) | (per_page == 0):
|
|
256
|
+
per_page = maxrows
|
|
257
|
+
startpage = (page - 1) * per_page
|
|
258
|
+
endpage = startpage + per_page
|
|
259
|
+
content_pages = int(np.ceil(len(loc) / per_page))
|
|
260
|
+
recs2send = tbl[loc[startpage:endpage]]
|
|
261
|
+
# Create new array
|
|
262
|
+
arrays = [recs2send[field] for field in columns]
|
|
263
|
+
recs2send = np.rec.fromarrays(arrays, dtype=dtype)
|
|
231
264
|
else:
|
|
232
265
|
# Apply pagination
|
|
233
266
|
maxrows = int(np.floor(MAX_RESPONSE_SIZE_BYTES / tbl.itemsize))
|
|
@@ -293,11 +326,13 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
293
326
|
if tbl.is_schemaless:
|
|
294
327
|
dict_list = tbl.get_dict_list(loc[startpage:endpage])
|
|
295
328
|
if columns:
|
|
329
|
+
# was built into `filtered_dict_list` and then dropped on
|
|
330
|
+
# the floor -- the UNfiltered list was sent to the client
|
|
296
331
|
columns = columns.split(',')
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
332
|
+
dict_list = [
|
|
333
|
+
{col: record[col] for col in columns if col in record}
|
|
334
|
+
for record in dict_list
|
|
335
|
+
]
|
|
301
336
|
|
|
302
337
|
json_payload = {
|
|
303
338
|
'names': names,
|
|
@@ -16,6 +16,7 @@ from SharedData.API.constants import (
|
|
|
16
16
|
DEFAULT_USER,
|
|
17
17
|
ERROR_SLEEP_SECONDS,
|
|
18
18
|
)
|
|
19
|
+
from SharedData.API.listcache import LIST_CACHE
|
|
19
20
|
from SharedData.API.utils import (
|
|
20
21
|
apply_subfolder,
|
|
21
22
|
make_empty_response,
|
|
@@ -45,22 +46,24 @@ def init_timeseries_routes(shdata, authenticate_fn):
|
|
|
45
46
|
return jsonify({'error': 'unauthorized'}), 401
|
|
46
47
|
keyword = request.args.get('keyword', '')
|
|
47
48
|
user = request.args.get('user', DEFAULT_USER)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
|
|
50
|
+
def _produce():
|
|
51
|
+
timeseries_df = shdata.list_timeseries(keyword=keyword, user=user)
|
|
52
|
+
if len(timeseries_df) == 0:
|
|
53
|
+
return b'' # sentinel -> 204
|
|
54
|
+
timeseries_list = timeseries_df.reset_index().to_dict('records')
|
|
55
|
+
for ts in timeseries_list:
|
|
56
|
+
for key, value in ts.items():
|
|
57
|
+
if pd.isna(value):
|
|
58
|
+
ts[key] = None
|
|
59
|
+
elif isinstance(value, pd.Timestamp):
|
|
60
|
+
ts[key] = value.isoformat()
|
|
61
|
+
return json.dumps(timeseries_list).encode('utf-8')
|
|
62
|
+
|
|
63
|
+
# Single-flight + TTL cache: see listcache.py.
|
|
64
|
+
response_data = LIST_CACHE.get(('timeseries', user, keyword), _produce)
|
|
65
|
+
if not response_data:
|
|
52
66
|
return Response(status=204)
|
|
53
|
-
|
|
54
|
-
timeseries_list = timeseries_df.reset_index().to_dict('records')
|
|
55
|
-
|
|
56
|
-
for ts in timeseries_list:
|
|
57
|
-
for key, value in ts.items():
|
|
58
|
-
if pd.isna(value):
|
|
59
|
-
ts[key] = None
|
|
60
|
-
elif isinstance(value, pd.Timestamp):
|
|
61
|
-
ts[key] = value.isoformat()
|
|
62
|
-
|
|
63
|
-
response_data = json.dumps(timeseries_list).encode('utf-8')
|
|
64
67
|
response = Response(response_data, mimetype='application/json')
|
|
65
68
|
response.headers['Content-Length'] = len(response_data)
|
|
66
69
|
return response
|