shareddata 6.85.7__tar.gz → 6.85.9__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.85.7/src/shareddata.egg-info → shareddata-6.85.9}/PKG-INFO +1 -1
- {shareddata-6.85.7 → shareddata-6.85.9}/setup.py +1 -1
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/cache.py +11 -1
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/SaveTables.py +1 -1
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/SharedData.py +5 -1
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/SharedNumpy.py +2 -2
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Table.py +269 -32
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableRewrite.py +104 -6
- {shareddata-6.85.7 → shareddata-6.85.9/src/shareddata.egg-info}/PKG-INFO +1 -1
- {shareddata-6.85.7 → shareddata-6.85.9}/LICENSE +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/MANIFEST.in +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/README.md +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/pyproject.toml +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/setup.cfg +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/apidocs-dark.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/apidocs.yml +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/auth.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/download_paths.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/download_slots.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/install/worker.ps1 +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/install/worker.sh +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/collections.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/data.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/downloads.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/operations.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/schedules.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/sqlite.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/tables.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/timeseries.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/web_routes.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/bootstrap.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/migrate.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/store.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/workers.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/catalog.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/config.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/errors.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/indexes.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/readers.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/resolve.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/service.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Database.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Defaults.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ClientAPI.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/LogReader.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/MongoDBClient.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Listing.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/LockWaiters.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Logger.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MCP/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MCP/__main__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MCP/client.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MCP/server.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Redaction.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ContainerHealth.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/JobStore.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/RoutineCommand.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/RoutineOrigin.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/Schedule.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ScheduleMirror.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ScheduleStore.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/Worker.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/WorkerLib.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/WorkerPool.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/SQLite.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableDisk.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndex.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitFunctions.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitGenerate.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableProtocol.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableRead.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Users.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Utils.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/activity_logger.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/app.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/google_logo.png +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/kafka_keys.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/logs.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/sched.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/styles.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/auth.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/backends.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/config.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/containers.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/identity.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/kafka_view.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/mongo_view.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/proc.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/redis_view.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/route_health.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/route_registry.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/sched.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/schedule_editor.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/server.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/services.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/shell.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/standalone.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/account.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/custom.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/favicon.ico +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/favicon.png +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/logo.png +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/logo_96x96.png +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/shell.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/shell.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/standalone.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/standalone.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/suggest.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/account.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/base.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/claim.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/claim_done.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/dash.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/index.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/login.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/login_2fa.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/partials/navbar.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/partials/scripts.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/recovery_codes.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/shell.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/standalone.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/twofactor.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/utils.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/workerpool.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/build_info.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/fleet.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/preflight.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/private_files.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/release_backup.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/server.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/services.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/SOURCES.txt +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/entry_points.txt +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/top_level.txt +0 -0
|
@@ -8,6 +8,9 @@ import datetime
|
|
|
8
8
|
import json
|
|
9
9
|
import time
|
|
10
10
|
|
|
11
|
+
import numpy as np
|
|
12
|
+
from bson.objectid import ObjectId
|
|
13
|
+
|
|
11
14
|
from flask import Blueprint, Response, jsonify, request
|
|
12
15
|
|
|
13
16
|
from SharedData.Logger import Logger
|
|
@@ -20,10 +23,17 @@ from SharedData.API.utils import (
|
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class _DatetimeEncoder(json.JSONEncoder):
|
|
23
|
-
"""JSON encoder
|
|
26
|
+
"""JSON encoder for cached values: datetime/date as ISO 8601 strings, a Mongo ObjectId as its hex
|
|
27
|
+
string, NumPy scalars and arrays as plain numbers and lists."""
|
|
24
28
|
def default(self, obj):
|
|
25
29
|
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
26
30
|
return obj.isoformat()
|
|
31
|
+
if isinstance(obj, ObjectId):
|
|
32
|
+
return str(obj)
|
|
33
|
+
if isinstance(obj, np.generic):
|
|
34
|
+
return obj.item()
|
|
35
|
+
if isinstance(obj, np.ndarray):
|
|
36
|
+
return obj.tolist()
|
|
27
37
|
return super().default(obj)
|
|
28
38
|
|
|
29
39
|
|
|
@@ -55,7 +55,7 @@ def write_table(dftable_row: Dict[str, Any]) -> str:
|
|
|
55
55
|
dftable_row['source'],
|
|
56
56
|
tablename
|
|
57
57
|
)
|
|
58
|
-
tbl.write(force_write=True)
|
|
58
|
+
tbl.write(force_write=True, strict=True)
|
|
59
59
|
return f"Success: {tablename}"
|
|
60
60
|
except Exception as e:
|
|
61
61
|
Logger.log.error(f"Error saving {dftable_row} {e}")
|
|
@@ -253,7 +253,11 @@ class SharedData:
|
|
|
253
253
|
elif len(partitioningstr) == 8:
|
|
254
254
|
partitioning = 'daily'
|
|
255
255
|
else:
|
|
256
|
-
|
|
256
|
+
# A new indexed table of a period shorter than a day must be partitioned. Opening an existing
|
|
257
|
+
# one (no schema given) is not creating: legacy unpartitioned tables stay readable and savable.
|
|
258
|
+
creating = (names is not None or formats is not None or value is not None
|
|
259
|
+
or is_schemaless or size is not None)
|
|
260
|
+
if hasindex and creating:
|
|
257
261
|
if period != 'D1':
|
|
258
262
|
raise Exception(f'Please specify partitioning for {period} tables')
|
|
259
263
|
|
|
@@ -285,7 +285,7 @@ class SharedNumpy(np.ndarray):
|
|
|
285
285
|
"""
|
|
286
286
|
self.table.trim()
|
|
287
287
|
|
|
288
|
-
def write(self,force_write=False):
|
|
288
|
+
def write(self,force_write=False,strict=False):
|
|
289
289
|
"""
|
|
290
290
|
Writes the table data to storage, optionally forcing the write operation.
|
|
291
291
|
|
|
@@ -300,7 +300,7 @@ class SharedNumpy(np.ndarray):
|
|
|
300
300
|
if (self.table.type != 1 or sys.platform == 'linux') and (
|
|
301
301
|
self.table.hdr['hasindex'] == 1 and self.table.hdr['isidxsorted'] == 0):
|
|
302
302
|
self.sort_index()
|
|
303
|
-
self.table.write(force_write)
|
|
303
|
+
self.table.write(force_write, strict)
|
|
304
304
|
|
|
305
305
|
def rememmap(self):
|
|
306
306
|
"""
|
|
@@ -22,6 +22,7 @@ import uuid
|
|
|
22
22
|
|
|
23
23
|
from SharedData.Logger import Logger
|
|
24
24
|
from SharedData.LockWaiters import TableBusy
|
|
25
|
+
from SharedData.TableRewrite import BsonIntegrityError
|
|
25
26
|
from SharedData.IO.AWSS3 import S3Upload, S3Download, S3ListFolder, S3GetMtime, S3DeleteFile, UpdateModTime
|
|
26
27
|
from SharedData.TableIndex import TableIndex
|
|
27
28
|
from SharedData.SharedNumpy import SharedNumpy
|
|
@@ -34,6 +35,17 @@ from SharedData.TableProtocol import (IDLE, WRITE, STRUCT, DIRTY, mode, epoch,
|
|
|
34
35
|
writer_lock, take_thread_lock, structure, structural)
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
# A BSON side file is compacted before every upload that has something to return: a table with nothing
|
|
39
|
+
# orphaned (append-only) has nothing to defragment, and rewriting it would only copy identical bytes.
|
|
40
|
+
BSON_DEFRAG_MIN_BYTES = 64 * 1024
|
|
41
|
+
BSON_DEFRAG_MIN_FRACTION = 0.0
|
|
42
|
+
# The rewrite holds the table lock and copies every live byte: for a large BSON file that must buy
|
|
43
|
+
# something. Above BSON_DEFRAG_LARGE_BYTES of allocated BSON, compact once at least this fraction is orphaned
|
|
44
|
+
# (a 43 GB file with 23 MB orphaned is 0.05%: not worth minutes of blocked ingestion).
|
|
45
|
+
BSON_DEFRAG_LARGE_BYTES = 1024 * 1024 * 1024
|
|
46
|
+
BSON_DEFRAG_LARGE_MIN_FRACTION = 0.01
|
|
47
|
+
|
|
48
|
+
|
|
37
49
|
class TableNotFound(FileNotFoundError):
|
|
38
50
|
"""The requested table is absent locally and remotely, with no creation schema."""
|
|
39
51
|
def __init__(self, table):
|
|
@@ -1373,6 +1385,23 @@ class Table:
|
|
|
1373
1385
|
return h1[:l] == h2[:l]
|
|
1374
1386
|
|
|
1375
1387
|
############### BSON MANAGEMENT ###############
|
|
1388
|
+
def _reset_bson_mapping(self):
|
|
1389
|
+
"""Drop the BSON mapping and reopen the file by path: after a paired publication the old
|
|
1390
|
+
mapping belongs to an unlinked inode and its offsets no longer match the records."""
|
|
1391
|
+
if self.bson_mmap is not None:
|
|
1392
|
+
try:
|
|
1393
|
+
self.bson_mmap.close()
|
|
1394
|
+
except (BufferError, ValueError):
|
|
1395
|
+
pass
|
|
1396
|
+
self.bson_mmap = None
|
|
1397
|
+
if self.bson_file_handle is not None:
|
|
1398
|
+
self.bson_file_handle.close()
|
|
1399
|
+
self.bson_file_handle = None
|
|
1400
|
+
self.curr_bson_size = 0
|
|
1401
|
+
self.prev_bson_ptr = 0
|
|
1402
|
+
if self.is_schemaless and self.bsonpath.exists() and self.bsonpath.stat().st_size > 0:
|
|
1403
|
+
self.init_bson_mmap()
|
|
1404
|
+
|
|
1376
1405
|
def init_bson_mmap(self, from_download=False):
|
|
1377
1406
|
"""Initialize or reopen memory-mapped BSON file with proper sizing.
|
|
1378
1407
|
|
|
@@ -1572,6 +1601,193 @@ class Table:
|
|
|
1572
1601
|
data = bytes(self.bson_mmap[offset:offset + size])
|
|
1573
1602
|
return bson.decode(data)
|
|
1574
1603
|
|
|
1604
|
+
def _bson_columns(self, count):
|
|
1605
|
+
"""bson_ptr and bson_size of the committed rows as int64 (legacy tables declare them '|V8')."""
|
|
1606
|
+
pointers = self.records['bson_ptr'][:count]
|
|
1607
|
+
sizes = self.records['bson_size'][:count]
|
|
1608
|
+
if sizes.dtype.kind == 'V' and sizes.dtype.itemsize == 8:
|
|
1609
|
+
sizes = np.ascontiguousarray(sizes).view(np.int64)
|
|
1610
|
+
if pointers.dtype.kind == 'V' and pointers.dtype.itemsize == 8:
|
|
1611
|
+
pointers = np.ascontiguousarray(pointers).view(np.int64)
|
|
1612
|
+
return np.asarray(pointers, dtype=np.int64), np.asarray(sizes, dtype=np.int64)
|
|
1613
|
+
|
|
1614
|
+
def bson_usage(self):
|
|
1615
|
+
"""What the BSON file holds: bytes referenced by live rows, the span allocated, the file size."""
|
|
1616
|
+
count = int(self.hdr['count'])
|
|
1617
|
+
file_size = self.bsonpath.stat().st_size if self.bsonpath.exists() else 0
|
|
1618
|
+
usage = dict(file_size=file_size, used=0, live=0, orphaned=0, live_rows=0, count=count)
|
|
1619
|
+
if count == 0:
|
|
1620
|
+
usage['used'] = usage['orphaned'] = int(self.hdr['next_bson_ptr'])
|
|
1621
|
+
return usage
|
|
1622
|
+
pointers, sizes = self._bson_columns(count)
|
|
1623
|
+
live = sizes > 0
|
|
1624
|
+
usage['live_rows'] = int(live.sum())
|
|
1625
|
+
usage['used'] = int(self.hdr['next_bson_ptr']) # a cursor past cleared documents is orphaned space
|
|
1626
|
+
if usage['live_rows']:
|
|
1627
|
+
usage['live'] = int(sizes[live].sum())
|
|
1628
|
+
usage['used'] = max(int((pointers[live] + sizes[live]).max()), usage['used'])
|
|
1629
|
+
usage['orphaned'] = max(usage['used'] - usage['live'], 0)
|
|
1630
|
+
return usage
|
|
1631
|
+
|
|
1632
|
+
def _bson_mismatches(self, pointers, sizes, live_idx):
|
|
1633
|
+
"""Rows whose pointer does not lead to a BSON document of exactly the recorded size: the int32 at
|
|
1634
|
+
the pointer is the document length, and a document ends with a zero byte."""
|
|
1635
|
+
import mmap
|
|
1636
|
+
bad = []
|
|
1637
|
+
if not len(live_idx):
|
|
1638
|
+
return np.zeros(0, dtype=np.int64)
|
|
1639
|
+
with open(self.bsonpath, 'rb') as source:
|
|
1640
|
+
view = mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ)
|
|
1641
|
+
data = None
|
|
1642
|
+
try:
|
|
1643
|
+
data = np.frombuffer(view, dtype=np.uint8)
|
|
1644
|
+
span = np.arange(4)
|
|
1645
|
+
for first in range(0, len(live_idx), 1 << 20):
|
|
1646
|
+
rows = live_idx[first:first + (1 << 20)]
|
|
1647
|
+
short = sizes[rows] < 5 # the smallest document, {}, is 5 bytes
|
|
1648
|
+
bad.append(rows[short])
|
|
1649
|
+
rows = rows[~short]
|
|
1650
|
+
start, length = pointers[rows], sizes[rows]
|
|
1651
|
+
head = data[start[:, None] + span].astype(np.int64)
|
|
1652
|
+
declared = head[:, 0] | (head[:, 1] << 8) | (head[:, 2] << 16) | (head[:, 3] << 24)
|
|
1653
|
+
ends_with_zero = data[start + length - 1] == 0
|
|
1654
|
+
bad.append(rows[(declared != length) | ~ends_with_zero])
|
|
1655
|
+
finally:
|
|
1656
|
+
data = None # release the buffer export before closing the map
|
|
1657
|
+
view.close()
|
|
1658
|
+
return np.concatenate(bad) if bad else np.zeros(0, dtype=np.int64)
|
|
1659
|
+
|
|
1660
|
+
def compact_bson_if_worthwhile(self):
|
|
1661
|
+
"""Compact the BSON file when enough of it is orphaned; None when it is not worth a rewrite."""
|
|
1662
|
+
usage = self.bson_usage()
|
|
1663
|
+
if usage['orphaned'] < BSON_DEFRAG_MIN_BYTES \
|
|
1664
|
+
or usage['orphaned'] < BSON_DEFRAG_MIN_FRACTION * max(usage['used'], 1):
|
|
1665
|
+
return None
|
|
1666
|
+
if usage['used'] > BSON_DEFRAG_LARGE_BYTES \
|
|
1667
|
+
and usage['orphaned'] < BSON_DEFRAG_LARGE_MIN_FRACTION * usage['used']:
|
|
1668
|
+
return None
|
|
1669
|
+
return self.defragment_bson(acquire=False)
|
|
1670
|
+
|
|
1671
|
+
def _defragment_bson_disk(self, chunk_size, acquire):
|
|
1672
|
+
"""Compact bson.bin of a disk table and move every bson_ptr, as ONE crash-safe publication.
|
|
1673
|
+
|
|
1674
|
+
The compacted BSON and the record image with the new pointers are built beside the live files
|
|
1675
|
+
(nothing live is written), then published as a pair by TableRewrite.publish_rewrite: BSON first,
|
|
1676
|
+
records second, with a version 2 descriptor that recovery rolls forward. Other processes reopen
|
|
1677
|
+
both files when the epoch published on leaving STRUCT reaches them (_refresh_mapping).
|
|
1678
|
+
"""
|
|
1679
|
+
import mmap
|
|
1680
|
+
import hashlib
|
|
1681
|
+
from SharedData.TableRewrite import (RewriteError, copy_metadata, file_identity, fsync_directory,
|
|
1682
|
+
pending_path, publish_rewrite, require_rewrite_support, write_all)
|
|
1683
|
+
from SharedData.TableProtocol import structure
|
|
1684
|
+
require_rewrite_support()
|
|
1685
|
+
if not self.is_schemaless:
|
|
1686
|
+
raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
|
|
1687
|
+
if acquire:
|
|
1688
|
+
self.acquire()
|
|
1689
|
+
stage = None
|
|
1690
|
+
token = None
|
|
1691
|
+
try:
|
|
1692
|
+
self._sync_held_mapping()
|
|
1693
|
+
# The old BSON is the (old, old) recovery baseline: make its recent writes durable before any
|
|
1694
|
+
# replacement exists, or a power loss could keep durable old records over lost BSON bytes.
|
|
1695
|
+
if self.bson_mmap is not None:
|
|
1696
|
+
self.bson_mmap.flush()
|
|
1697
|
+
if self.bsonpath.exists():
|
|
1698
|
+
with open(self.bsonpath, 'rb') as baseline:
|
|
1699
|
+
os.fsync(baseline.fileno())
|
|
1700
|
+
count = int(self.hdr['count'])
|
|
1701
|
+
pointers, sizes = self._bson_columns(count) if count else (np.zeros(0, np.int64), np.zeros(0, np.int64))
|
|
1702
|
+
live_idx = np.flatnonzero(sizes > 0)
|
|
1703
|
+
if not self.bsonpath.exists():
|
|
1704
|
+
if len(live_idx):
|
|
1705
|
+
raise RewriteError(f'{self.relpath}: records reference a BSON file that does not exist')
|
|
1706
|
+
Logger.log.warning(f'No BSON file to defragment for {self.relpath}')
|
|
1707
|
+
return {'original_size': 0, 'new_size': 0, 'reclaimed': 0}
|
|
1708
|
+
original_size = self.bsonpath.stat().st_size
|
|
1709
|
+
if len(live_idx) and (np.any(pointers[live_idx] < 0) or np.any(pointers[live_idx] + sizes[live_idx] > original_size)):
|
|
1710
|
+
raise RewriteError(f'{self.relpath}: a record points outside its BSON file; nothing was changed')
|
|
1711
|
+
mismatched = self._bson_mismatches(pointers, sizes, live_idx)
|
|
1712
|
+
if len(mismatched):
|
|
1713
|
+
from SharedData.TableRewrite import BsonIntegrityError
|
|
1714
|
+
raise BsonIntegrityError(
|
|
1715
|
+
f'{self.relpath}: {len(mismatched)} of {len(live_idx)} records do not point at a BSON document '
|
|
1716
|
+
f'of their recorded size (first rows {mismatched[:5].tolist()}); not compacted, '
|
|
1717
|
+
'the unreferenced bytes may be the documents those records lost')
|
|
1718
|
+
new_offsets = np.zeros(len(live_idx), dtype=np.int64)
|
|
1719
|
+
if len(live_idx):
|
|
1720
|
+
new_offsets[1:] = np.cumsum(sizes[live_idx])[:-1]
|
|
1721
|
+
payload = int(sizes[live_idx].sum()) if len(live_idx) else 0
|
|
1722
|
+
token = uuid.uuid4().hex
|
|
1723
|
+
stage = self.bsonpath.parent / ('bson.bin.stage.' + token)
|
|
1724
|
+
digest = hashlib.sha256()
|
|
1725
|
+
fd = os.open(stage, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
|
|
1726
|
+
try:
|
|
1727
|
+
copy_metadata(self.bsonpath, fd)
|
|
1728
|
+
if len(live_idx):
|
|
1729
|
+
with open(self.bsonpath, 'rb') as source:
|
|
1730
|
+
view = mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ)
|
|
1731
|
+
try:
|
|
1732
|
+
message = f'Defragmenting BSON file: {self.relpath}'
|
|
1733
|
+
for first in tqdm(range(0, len(live_idx), chunk_size), desc=message, unit='chunk'):
|
|
1734
|
+
rows = live_idx[first:first + chunk_size]
|
|
1735
|
+
buffer = bytearray(int(sizes[rows].sum()))
|
|
1736
|
+
cursor = 0
|
|
1737
|
+
for pointer, size in zip(pointers[rows].tolist(), sizes[rows].tolist()):
|
|
1738
|
+
buffer[cursor:cursor + size] = view[pointer:pointer + size]
|
|
1739
|
+
cursor += size
|
|
1740
|
+
digest.update(buffer)
|
|
1741
|
+
write_all(fd, buffer)
|
|
1742
|
+
finally:
|
|
1743
|
+
view.close()
|
|
1744
|
+
if payload == 0: # keep the file mappable: a physical zero-length file is refused
|
|
1745
|
+
digest.update(b'\0')
|
|
1746
|
+
write_all(fd, b'\0')
|
|
1747
|
+
os.fsync(fd)
|
|
1748
|
+
finally:
|
|
1749
|
+
os.close(fd)
|
|
1750
|
+
fsync_directory(stage.parent)
|
|
1751
|
+
new_size = max(payload, 1)
|
|
1752
|
+
mapping = np.full(count, -1, dtype=np.int64)
|
|
1753
|
+
mapping[live_idx] = new_offsets
|
|
1754
|
+
|
|
1755
|
+
def move_pointers(chunk, first):
|
|
1756
|
+
moved = mapping[first:first + len(chunk)]
|
|
1757
|
+
selected = moved >= 0
|
|
1758
|
+
if selected.any():
|
|
1759
|
+
column = chunk['bson_ptr']
|
|
1760
|
+
if column.dtype.kind == 'V' and column.dtype.itemsize == 8:
|
|
1761
|
+
column = column.view(np.int64)
|
|
1762
|
+
column[selected] = moved[selected]
|
|
1763
|
+
return chunk
|
|
1764
|
+
|
|
1765
|
+
with structure(self):
|
|
1766
|
+
publish_rewrite(self, None, 'defrag', start=0, transform=move_pointers,
|
|
1767
|
+
header_updates={'next_bson_ptr': payload, 'mtime': max(datetime.now().timestamp(), float(self.hdr['mtime']) + 0.001)},
|
|
1768
|
+
bson=dict(token=token, stage=stage, sha256=digest.hexdigest(), size=new_size))
|
|
1769
|
+
stats = {'original_size': original_size, 'new_size': new_size,
|
|
1770
|
+
'reclaimed': original_size - new_size, 'live_bytes': payload, 'live_rows': int(len(live_idx))}
|
|
1771
|
+
Logger.log.info(
|
|
1772
|
+
f'Defragmented {self.relpath} BSON file: {original_size/1024/1024:.2f}MB -> '
|
|
1773
|
+
f'{new_size/1024/1024:.2f}MB (reclaimed {stats["reclaimed"]/1024/1024:.2f}MB)')
|
|
1774
|
+
return stats
|
|
1775
|
+
except Exception as e:
|
|
1776
|
+
Logger.log.error(f'Failed to defragment BSON file for {self.relpath}: {e}')
|
|
1777
|
+
# With a pending descriptor the stages belong to recovery; without one nothing refers to them.
|
|
1778
|
+
if token is not None and not pending_path(self).exists():
|
|
1779
|
+
for leftover in (self.bsonpath.parent / ('bson.bin.stage.' + token),
|
|
1780
|
+
Path(str(self.filepath) + '.stage.' + token),
|
|
1781
|
+
Path(str(self.filepath) + '.rewrite.prepare.' + token)):
|
|
1782
|
+
try:
|
|
1783
|
+
leftover.unlink()
|
|
1784
|
+
except OSError:
|
|
1785
|
+
pass
|
|
1786
|
+
raise
|
|
1787
|
+
finally:
|
|
1788
|
+
if acquire:
|
|
1789
|
+
self.release()
|
|
1790
|
+
|
|
1575
1791
|
def defragment_bson(self, chunk_size=100000, buffer_mb=512, acquire=True):
|
|
1576
1792
|
"""
|
|
1577
1793
|
Defragments the BSON file IN-PLACE by creating a compacted copy, then copying it back to the original file.
|
|
@@ -1603,8 +1819,7 @@ class Table:
|
|
|
1603
1819
|
Exception: If the table is not schemaless or if defragmentation fails.
|
|
1604
1820
|
"""
|
|
1605
1821
|
if self.type == 1:
|
|
1606
|
-
|
|
1607
|
-
raise RewriteError('disk BSON maintenance requires coordinated staged publication')
|
|
1822
|
+
return self._defragment_bson_disk(chunk_size, acquire)
|
|
1608
1823
|
if not self.is_schemaless:
|
|
1609
1824
|
raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
|
|
1610
1825
|
|
|
@@ -1795,7 +2010,7 @@ class Table:
|
|
|
1795
2010
|
self.release()
|
|
1796
2011
|
|
|
1797
2012
|
############### WRITE ###############
|
|
1798
|
-
def write(self, force_write=False):
|
|
2013
|
+
def write(self, force_write=False, strict=False):
|
|
1799
2014
|
"""
|
|
1800
2015
|
Writes data to a file and uploads it concurrently using separate threads.
|
|
1801
2016
|
|
|
@@ -1811,19 +2026,47 @@ class Table:
|
|
|
1811
2026
|
7. Handles exceptions by logging an error message and re-raising the exception.
|
|
1812
2027
|
8. Releases the lock regardless of success or failure.
|
|
1813
2028
|
|
|
2029
|
+
A schemaless table whose BSON file is about to be uploaded is compacted first, in this thread
|
|
2030
|
+
and under this lock (the compaction is a STRUCT publication, so it cannot run in the upload
|
|
2031
|
+
thread), and before the header, checksums and timestamp are prepared, so that the uploaded head
|
|
2032
|
+
and tail carry the moved pointers.
|
|
2033
|
+
|
|
1814
2034
|
Parameters:
|
|
1815
2035
|
force_write (bool): If True, forces the header to be written even if not modified.
|
|
2036
|
+
strict (bool): If True an upload or flush failure raises. It is always logged with the table name.
|
|
1816
2037
|
|
|
1817
2038
|
Raises:
|
|
1818
|
-
Exception: If the write or upload
|
|
2039
|
+
Exception: If the write fails (or, with strict, if the upload or the local flush fails).
|
|
1819
2040
|
"""
|
|
1820
2041
|
errmsg = ''
|
|
1821
2042
|
self.acquire()
|
|
1822
2043
|
try:
|
|
1823
2044
|
|
|
1824
2045
|
tini = time.time()
|
|
2046
|
+
from SharedData.services import s3_enabled
|
|
2047
|
+
remote = s3_enabled()
|
|
1825
2048
|
# create header
|
|
1826
2049
|
mtime = self.hdr['mtime']
|
|
2050
|
+
compacted = False
|
|
2051
|
+
if remote and self.is_schemaless and self.type == 1 and self.bsonpath.exists():
|
|
2052
|
+
remote_bson_mtime = S3GetMtime(str(self.bsonpath) + '.gzip', self.database_folder)
|
|
2053
|
+
if force_write or remote_bson_mtime is None or remote_bson_mtime < mtime:
|
|
2054
|
+
try:
|
|
2055
|
+
stats = self.compact_bson_if_worthwhile()
|
|
2056
|
+
except BsonIntegrityError as error:
|
|
2057
|
+
# Loud, every night, but the upload goes on as it was: nothing is rewritten.
|
|
2058
|
+
Logger.log.error('BSON compaction skipped: %s' % error)
|
|
2059
|
+
stats = None
|
|
2060
|
+
if stats is not None:
|
|
2061
|
+
compacted = True
|
|
2062
|
+
# New pointers: head, tail and BSON are all rewritten and all uploaded, under a
|
|
2063
|
+
# publication time later than anything the remote holds, so consumers see the change.
|
|
2064
|
+
newest = max([m for m in (remote_bson_mtime,
|
|
2065
|
+
S3GetMtime(str(self.headpath) + '.gzip', self.database_folder),
|
|
2066
|
+
S3GetMtime(str(self.tailpath) + '.gzip', self.database_folder))
|
|
2067
|
+
if m is not None] + [float(self.hdr['mtime'])])
|
|
2068
|
+
self.hdr['mtime'] = max(float(self.hdr['mtime']), newest + 0.001)
|
|
2069
|
+
mtime = self.hdr['mtime']
|
|
1827
2070
|
write_head = False
|
|
1828
2071
|
write_head = self.fill_header(force_write)
|
|
1829
2072
|
|
|
@@ -1833,21 +2076,32 @@ class Table:
|
|
|
1833
2076
|
if self.bson_file_handle is not None:
|
|
1834
2077
|
os.fsync(self.bson_file_handle.fileno())
|
|
1835
2078
|
|
|
1836
|
-
|
|
1837
|
-
if not s3_enabled():
|
|
2079
|
+
if not remote:
|
|
1838
2080
|
self.write_file()
|
|
1839
2081
|
return
|
|
1840
2082
|
|
|
2083
|
+
failures = []
|
|
2084
|
+
|
|
2085
|
+
def guarded(label, target, *args):
|
|
2086
|
+
# A thread's exception never reaches join(): keep it, name the table, let the caller decide.
|
|
2087
|
+
try:
|
|
2088
|
+
target(*args)
|
|
2089
|
+
except BaseException as error:
|
|
2090
|
+
failures.append('%s: %s' % (label, error))
|
|
2091
|
+
Logger.log.error('%s %s failed: %s' % (self.relpath, label, error))
|
|
2092
|
+
|
|
1841
2093
|
thread_s3 = threading.Thread(
|
|
1842
|
-
target=self.upload,
|
|
2094
|
+
target=guarded, args=('upload', self.upload, write_head, mtime, force_write or compacted, compacted))
|
|
1843
2095
|
thread_s3.start()
|
|
1844
2096
|
|
|
1845
|
-
thread_flush = threading.Thread(target=self.write_file)
|
|
2097
|
+
thread_flush = threading.Thread(target=guarded, args=('flush', self.write_file))
|
|
1846
2098
|
thread_flush.start()
|
|
1847
2099
|
|
|
1848
2100
|
# join threads
|
|
1849
2101
|
thread_s3.join()
|
|
1850
2102
|
thread_flush.join()
|
|
2103
|
+
if failures and strict:
|
|
2104
|
+
raise Exception('; '.join(failures))
|
|
1851
2105
|
|
|
1852
2106
|
te = time.time() - tini
|
|
1853
2107
|
datasize = self.hdr['count'] * self.hdr['itemsize'] / 1000000
|
|
@@ -2030,33 +2284,14 @@ class Table:
|
|
|
2030
2284
|
return write_head
|
|
2031
2285
|
|
|
2032
2286
|
############### UPLOAD ###############
|
|
2033
|
-
def upload(self, write_head, mtime, force_write=False):
|
|
2287
|
+
def upload(self, write_head, mtime, force_write=False, force_tail=False):
|
|
2034
2288
|
"""
|
|
2035
2289
|
Uploads the head, tail, and BSON files to remote storage based on modification times and flags.
|
|
2036
2290
|
|
|
2037
|
-
For schemaless tables
|
|
2038
|
-
|
|
2291
|
+
For schemaless tables the BSON file is uploaded after head and tail. Compaction is not done here:
|
|
2292
|
+
it is a structural publication and runs in write(), before the header is prepared.
|
|
2039
2293
|
"""
|
|
2040
|
-
#
|
|
2041
|
-
# This ensures the pointers in records are updated before we upload them
|
|
2042
|
-
if self.is_schemaless and self.bsonpath.exists():
|
|
2043
|
-
bson_gzip_path = str(self.bsonpath) + '.gzip'
|
|
2044
|
-
remote_bson_mtime = S3GetMtime(bson_gzip_path, self.database_folder)
|
|
2045
|
-
remote_bson_is_updated = False
|
|
2046
|
-
if remote_bson_mtime is not None:
|
|
2047
|
-
remote_bson_is_updated = remote_bson_mtime >= mtime
|
|
2048
|
-
|
|
2049
|
-
# Defragment if BSON needs upload (this updates pointers in records)
|
|
2050
|
-
if not remote_bson_is_updated or force_write:
|
|
2051
|
-
# Pass acquire=False because we're already in write() context with lock held
|
|
2052
|
-
stats = self.defragment_bson(acquire=False)
|
|
2053
|
-
|
|
2054
|
-
# Mark header as modified to force head/tail upload with new pointers
|
|
2055
|
-
write_head = True
|
|
2056
|
-
self.hdr['mtimehead'] = mtime
|
|
2057
|
-
self.hdr['mtimetail'] = mtime
|
|
2058
|
-
|
|
2059
|
-
# Now upload head/tail with UPDATED pointers
|
|
2294
|
+
# head/tail carry the pointers of the BSON uploaded below (compacted by write() when it was worth it)
|
|
2060
2295
|
remote_head_mtime = S3GetMtime(str(self.headpath)+'.gzip', self.database_folder)
|
|
2061
2296
|
remote_head_is_updated = False
|
|
2062
2297
|
if not remote_head_mtime is None:
|
|
@@ -2070,7 +2305,7 @@ class Table:
|
|
|
2070
2305
|
remote_tail_is_updated = False
|
|
2071
2306
|
if not remote_tail_mtime is None:
|
|
2072
2307
|
remote_tail_is_updated = remote_tail_mtime >= self.hdr['mtimetail']
|
|
2073
|
-
if not remote_tail_is_updated:
|
|
2308
|
+
if force_tail or not remote_tail_is_updated:
|
|
2074
2309
|
self.upload_tail(mtime)
|
|
2075
2310
|
else:
|
|
2076
2311
|
self.delete_tail()
|
|
@@ -2741,6 +2976,8 @@ class Table:
|
|
|
2741
2976
|
if (dirty or self._mapping_epoch != current
|
|
2742
2977
|
or self.size != int(self.hdr['recordssize'])):
|
|
2743
2978
|
self.attach()
|
|
2979
|
+
if getattr(self, 'is_schemaless', False):
|
|
2980
|
+
self._reset_bson_mapping() # the BSON file is published together with data.bin
|
|
2744
2981
|
if self.hasindex and not dirty:
|
|
2745
2982
|
self.index.rememmap()
|
|
2746
2983
|
self._mapping_epoch = current
|