shareddata 6.85.7__tar.gz → 6.85.8__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.8}/PKG-INFO +1 -1
- {shareddata-6.85.7 → shareddata-6.85.8}/setup.py +1 -1
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/cache.py +11 -1
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/SaveTables.py +1 -1
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/SharedData.py +5 -1
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/SharedNumpy.py +2 -2
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Table.py +219 -32
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableRewrite.py +99 -6
- {shareddata-6.85.7 → shareddata-6.85.8/src/shareddata.egg-info}/PKG-INFO +1 -1
- {shareddata-6.85.7 → shareddata-6.85.8}/LICENSE +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/MANIFEST.in +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/README.md +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/pyproject.toml +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/setup.cfg +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/apidocs-dark.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/apidocs.yml +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/auth.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/download_paths.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/download_slots.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/install/worker.ps1 +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/install/worker.sh +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/collections.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/data.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/downloads.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/operations.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/schedules.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/sqlite.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/tables.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/timeseries.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/web_routes.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/bootstrap.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/migrate.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/store.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/workers.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/catalog.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/config.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/errors.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/indexes.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/readers.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/resolve.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/service.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Database.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Defaults.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ClientAPI.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/LogReader.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/MongoDBClient.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Listing.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/LockWaiters.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Logger.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MCP/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MCP/__main__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MCP/client.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MCP/server.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Redaction.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ContainerHealth.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/JobStore.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/RoutineCommand.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/RoutineOrigin.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/Schedule.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ScheduleMirror.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ScheduleStore.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/Worker.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/WorkerLib.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/WorkerPool.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/SQLite.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableDisk.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndex.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitFunctions.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitGenerate.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableProtocol.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableRead.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Users.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Utils.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/activity_logger.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/app.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/google_logo.png +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/kafka_keys.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/logs.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/sched.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/styles.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/auth.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/backends.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/config.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/containers.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/identity.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/kafka_view.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/mongo_view.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/proc.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/redis_view.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/route_health.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/route_registry.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/sched.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/schedule_editor.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/server.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/services.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/shell.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/standalone.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/account.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/custom.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/favicon.ico +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/favicon.png +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/logo.png +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/logo_96x96.png +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/shell.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/shell.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/standalone.css +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/standalone.js +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/suggest.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/account.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/base.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/claim.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/claim_done.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/dash.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/index.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/login.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/login_2fa.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/partials/navbar.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/partials/scripts.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/recovery_codes.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/shell.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/standalone.html +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/twofactor.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/utils.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/workerpool.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/__init__.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/build_info.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/fleet.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/preflight.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/private_files.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/release_backup.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/server.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/services.py +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/SOURCES.txt +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/entry_points.txt +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.85.7 → shareddata-6.85.8}/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
|
"""
|
|
@@ -34,6 +34,12 @@ from SharedData.TableProtocol import (IDLE, WRITE, STRUCT, DIRTY, mode, epoch,
|
|
|
34
34
|
writer_lock, take_thread_lock, structure, structural)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
# A BSON side file is compacted before every upload that has something to return: a table with nothing
|
|
38
|
+
# orphaned (append-only) has nothing to defragment, and rewriting it would only copy identical bytes.
|
|
39
|
+
BSON_DEFRAG_MIN_BYTES = 64 * 1024
|
|
40
|
+
BSON_DEFRAG_MIN_FRACTION = 0.0
|
|
41
|
+
|
|
42
|
+
|
|
37
43
|
class TableNotFound(FileNotFoundError):
|
|
38
44
|
"""The requested table is absent locally and remotely, with no creation schema."""
|
|
39
45
|
def __init__(self, table):
|
|
@@ -1373,6 +1379,23 @@ class Table:
|
|
|
1373
1379
|
return h1[:l] == h2[:l]
|
|
1374
1380
|
|
|
1375
1381
|
############### BSON MANAGEMENT ###############
|
|
1382
|
+
def _reset_bson_mapping(self):
|
|
1383
|
+
"""Drop the BSON mapping and reopen the file by path: after a paired publication the old
|
|
1384
|
+
mapping belongs to an unlinked inode and its offsets no longer match the records."""
|
|
1385
|
+
if self.bson_mmap is not None:
|
|
1386
|
+
try:
|
|
1387
|
+
self.bson_mmap.close()
|
|
1388
|
+
except (BufferError, ValueError):
|
|
1389
|
+
pass
|
|
1390
|
+
self.bson_mmap = None
|
|
1391
|
+
if self.bson_file_handle is not None:
|
|
1392
|
+
self.bson_file_handle.close()
|
|
1393
|
+
self.bson_file_handle = None
|
|
1394
|
+
self.curr_bson_size = 0
|
|
1395
|
+
self.prev_bson_ptr = 0
|
|
1396
|
+
if self.is_schemaless and self.bsonpath.exists() and self.bsonpath.stat().st_size > 0:
|
|
1397
|
+
self.init_bson_mmap()
|
|
1398
|
+
|
|
1376
1399
|
def init_bson_mmap(self, from_download=False):
|
|
1377
1400
|
"""Initialize or reopen memory-mapped BSON file with proper sizing.
|
|
1378
1401
|
|
|
@@ -1572,6 +1595,155 @@ class Table:
|
|
|
1572
1595
|
data = bytes(self.bson_mmap[offset:offset + size])
|
|
1573
1596
|
return bson.decode(data)
|
|
1574
1597
|
|
|
1598
|
+
def _bson_columns(self, count):
|
|
1599
|
+
"""bson_ptr and bson_size of the committed rows as int64 (legacy tables declare them '|V8')."""
|
|
1600
|
+
pointers = self.records['bson_ptr'][:count]
|
|
1601
|
+
sizes = self.records['bson_size'][:count]
|
|
1602
|
+
if sizes.dtype.kind == 'V' and sizes.dtype.itemsize == 8:
|
|
1603
|
+
sizes = np.ascontiguousarray(sizes).view(np.int64)
|
|
1604
|
+
if pointers.dtype.kind == 'V' and pointers.dtype.itemsize == 8:
|
|
1605
|
+
pointers = np.ascontiguousarray(pointers).view(np.int64)
|
|
1606
|
+
return np.asarray(pointers, dtype=np.int64), np.asarray(sizes, dtype=np.int64)
|
|
1607
|
+
|
|
1608
|
+
def bson_usage(self):
|
|
1609
|
+
"""What the BSON file holds: bytes referenced by live rows, the span allocated, the file size."""
|
|
1610
|
+
count = int(self.hdr['count'])
|
|
1611
|
+
file_size = self.bsonpath.stat().st_size if self.bsonpath.exists() else 0
|
|
1612
|
+
usage = dict(file_size=file_size, used=0, live=0, orphaned=0, live_rows=0, count=count)
|
|
1613
|
+
if count == 0:
|
|
1614
|
+
usage['used'] = usage['orphaned'] = int(self.hdr['next_bson_ptr'])
|
|
1615
|
+
return usage
|
|
1616
|
+
pointers, sizes = self._bson_columns(count)
|
|
1617
|
+
live = sizes > 0
|
|
1618
|
+
usage['live_rows'] = int(live.sum())
|
|
1619
|
+
usage['used'] = int(self.hdr['next_bson_ptr']) # a cursor past cleared documents is orphaned space
|
|
1620
|
+
if usage['live_rows']:
|
|
1621
|
+
usage['live'] = int(sizes[live].sum())
|
|
1622
|
+
usage['used'] = max(int((pointers[live] + sizes[live]).max()), usage['used'])
|
|
1623
|
+
usage['orphaned'] = max(usage['used'] - usage['live'], 0)
|
|
1624
|
+
return usage
|
|
1625
|
+
|
|
1626
|
+
def compact_bson_if_worthwhile(self):
|
|
1627
|
+
"""Compact the BSON file when enough of it is orphaned; None when it is not worth a rewrite."""
|
|
1628
|
+
usage = self.bson_usage()
|
|
1629
|
+
if usage['orphaned'] < BSON_DEFRAG_MIN_BYTES \
|
|
1630
|
+
or usage['orphaned'] < BSON_DEFRAG_MIN_FRACTION * max(usage['used'], 1):
|
|
1631
|
+
return None
|
|
1632
|
+
return self.defragment_bson(acquire=False)
|
|
1633
|
+
|
|
1634
|
+
def _defragment_bson_disk(self, chunk_size, acquire):
|
|
1635
|
+
"""Compact bson.bin of a disk table and move every bson_ptr, as ONE crash-safe publication.
|
|
1636
|
+
|
|
1637
|
+
The compacted BSON and the record image with the new pointers are built beside the live files
|
|
1638
|
+
(nothing live is written), then published as a pair by TableRewrite.publish_rewrite: BSON first,
|
|
1639
|
+
records second, with a version 2 descriptor that recovery rolls forward. Other processes reopen
|
|
1640
|
+
both files when the epoch published on leaving STRUCT reaches them (_refresh_mapping).
|
|
1641
|
+
"""
|
|
1642
|
+
import mmap
|
|
1643
|
+
import hashlib
|
|
1644
|
+
from SharedData.TableRewrite import (RewriteError, copy_metadata, file_identity, fsync_directory,
|
|
1645
|
+
pending_path, publish_rewrite, require_rewrite_support, write_all)
|
|
1646
|
+
from SharedData.TableProtocol import structure
|
|
1647
|
+
require_rewrite_support()
|
|
1648
|
+
if not self.is_schemaless:
|
|
1649
|
+
raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
|
|
1650
|
+
if acquire:
|
|
1651
|
+
self.acquire()
|
|
1652
|
+
stage = None
|
|
1653
|
+
token = None
|
|
1654
|
+
try:
|
|
1655
|
+
self._sync_held_mapping()
|
|
1656
|
+
# The old BSON is the (old, old) recovery baseline: make its recent writes durable before any
|
|
1657
|
+
# replacement exists, or a power loss could keep durable old records over lost BSON bytes.
|
|
1658
|
+
if self.bson_mmap is not None:
|
|
1659
|
+
self.bson_mmap.flush()
|
|
1660
|
+
if self.bsonpath.exists():
|
|
1661
|
+
with open(self.bsonpath, 'rb') as baseline:
|
|
1662
|
+
os.fsync(baseline.fileno())
|
|
1663
|
+
count = int(self.hdr['count'])
|
|
1664
|
+
pointers, sizes = self._bson_columns(count) if count else (np.zeros(0, np.int64), np.zeros(0, np.int64))
|
|
1665
|
+
live_idx = np.flatnonzero(sizes > 0)
|
|
1666
|
+
if not self.bsonpath.exists():
|
|
1667
|
+
if len(live_idx):
|
|
1668
|
+
raise RewriteError(f'{self.relpath}: records reference a BSON file that does not exist')
|
|
1669
|
+
Logger.log.warning(f'No BSON file to defragment for {self.relpath}')
|
|
1670
|
+
return {'original_size': 0, 'new_size': 0, 'reclaimed': 0}
|
|
1671
|
+
original_size = self.bsonpath.stat().st_size
|
|
1672
|
+
if len(live_idx) and (np.any(pointers[live_idx] < 0) or np.any(pointers[live_idx] + sizes[live_idx] > original_size)):
|
|
1673
|
+
raise RewriteError(f'{self.relpath}: a record points outside its BSON file; nothing was changed')
|
|
1674
|
+
new_offsets = np.zeros(len(live_idx), dtype=np.int64)
|
|
1675
|
+
if len(live_idx):
|
|
1676
|
+
new_offsets[1:] = np.cumsum(sizes[live_idx])[:-1]
|
|
1677
|
+
payload = int(sizes[live_idx].sum()) if len(live_idx) else 0
|
|
1678
|
+
token = uuid.uuid4().hex
|
|
1679
|
+
stage = self.bsonpath.parent / ('bson.bin.stage.' + token)
|
|
1680
|
+
digest = hashlib.sha256()
|
|
1681
|
+
fd = os.open(stage, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
|
|
1682
|
+
try:
|
|
1683
|
+
copy_metadata(self.bsonpath, fd)
|
|
1684
|
+
if len(live_idx):
|
|
1685
|
+
with open(self.bsonpath, 'rb') as source:
|
|
1686
|
+
view = mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ)
|
|
1687
|
+
try:
|
|
1688
|
+
message = f'Defragmenting BSON file: {self.relpath}'
|
|
1689
|
+
for first in tqdm(range(0, len(live_idx), chunk_size), desc=message, unit='chunk'):
|
|
1690
|
+
rows = live_idx[first:first + chunk_size]
|
|
1691
|
+
buffer = bytearray(int(sizes[rows].sum()))
|
|
1692
|
+
cursor = 0
|
|
1693
|
+
for pointer, size in zip(pointers[rows].tolist(), sizes[rows].tolist()):
|
|
1694
|
+
buffer[cursor:cursor + size] = view[pointer:pointer + size]
|
|
1695
|
+
cursor += size
|
|
1696
|
+
digest.update(buffer)
|
|
1697
|
+
write_all(fd, buffer)
|
|
1698
|
+
finally:
|
|
1699
|
+
view.close()
|
|
1700
|
+
if payload == 0: # keep the file mappable: a physical zero-length file is refused
|
|
1701
|
+
digest.update(b'\0')
|
|
1702
|
+
write_all(fd, b'\0')
|
|
1703
|
+
os.fsync(fd)
|
|
1704
|
+
finally:
|
|
1705
|
+
os.close(fd)
|
|
1706
|
+
fsync_directory(stage.parent)
|
|
1707
|
+
new_size = max(payload, 1)
|
|
1708
|
+
mapping = np.full(count, -1, dtype=np.int64)
|
|
1709
|
+
mapping[live_idx] = new_offsets
|
|
1710
|
+
|
|
1711
|
+
def move_pointers(chunk, first):
|
|
1712
|
+
moved = mapping[first:first + len(chunk)]
|
|
1713
|
+
selected = moved >= 0
|
|
1714
|
+
if selected.any():
|
|
1715
|
+
column = chunk['bson_ptr']
|
|
1716
|
+
if column.dtype.kind == 'V' and column.dtype.itemsize == 8:
|
|
1717
|
+
column = column.view(np.int64)
|
|
1718
|
+
column[selected] = moved[selected]
|
|
1719
|
+
return chunk
|
|
1720
|
+
|
|
1721
|
+
with structure(self):
|
|
1722
|
+
publish_rewrite(self, None, 'defrag', start=0, transform=move_pointers,
|
|
1723
|
+
header_updates={'next_bson_ptr': payload, 'mtime': max(datetime.now().timestamp(), float(self.hdr['mtime']) + 0.001)},
|
|
1724
|
+
bson=dict(token=token, stage=stage, sha256=digest.hexdigest(), size=new_size))
|
|
1725
|
+
stats = {'original_size': original_size, 'new_size': new_size,
|
|
1726
|
+
'reclaimed': original_size - new_size, 'live_bytes': payload, 'live_rows': int(len(live_idx))}
|
|
1727
|
+
Logger.log.info(
|
|
1728
|
+
f'Defragmented {self.relpath} BSON file: {original_size/1024/1024:.2f}MB -> '
|
|
1729
|
+
f'{new_size/1024/1024:.2f}MB (reclaimed {stats["reclaimed"]/1024/1024:.2f}MB)')
|
|
1730
|
+
return stats
|
|
1731
|
+
except Exception as e:
|
|
1732
|
+
Logger.log.error(f'Failed to defragment BSON file for {self.relpath}: {e}')
|
|
1733
|
+
# With a pending descriptor the stages belong to recovery; without one nothing refers to them.
|
|
1734
|
+
if token is not None and not pending_path(self).exists():
|
|
1735
|
+
for leftover in (self.bsonpath.parent / ('bson.bin.stage.' + token),
|
|
1736
|
+
Path(str(self.filepath) + '.stage.' + token),
|
|
1737
|
+
Path(str(self.filepath) + '.rewrite.prepare.' + token)):
|
|
1738
|
+
try:
|
|
1739
|
+
leftover.unlink()
|
|
1740
|
+
except OSError:
|
|
1741
|
+
pass
|
|
1742
|
+
raise
|
|
1743
|
+
finally:
|
|
1744
|
+
if acquire:
|
|
1745
|
+
self.release()
|
|
1746
|
+
|
|
1575
1747
|
def defragment_bson(self, chunk_size=100000, buffer_mb=512, acquire=True):
|
|
1576
1748
|
"""
|
|
1577
1749
|
Defragments the BSON file IN-PLACE by creating a compacted copy, then copying it back to the original file.
|
|
@@ -1603,8 +1775,7 @@ class Table:
|
|
|
1603
1775
|
Exception: If the table is not schemaless or if defragmentation fails.
|
|
1604
1776
|
"""
|
|
1605
1777
|
if self.type == 1:
|
|
1606
|
-
|
|
1607
|
-
raise RewriteError('disk BSON maintenance requires coordinated staged publication')
|
|
1778
|
+
return self._defragment_bson_disk(chunk_size, acquire)
|
|
1608
1779
|
if not self.is_schemaless:
|
|
1609
1780
|
raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
|
|
1610
1781
|
|
|
@@ -1795,7 +1966,7 @@ class Table:
|
|
|
1795
1966
|
self.release()
|
|
1796
1967
|
|
|
1797
1968
|
############### WRITE ###############
|
|
1798
|
-
def write(self, force_write=False):
|
|
1969
|
+
def write(self, force_write=False, strict=False):
|
|
1799
1970
|
"""
|
|
1800
1971
|
Writes data to a file and uploads it concurrently using separate threads.
|
|
1801
1972
|
|
|
@@ -1811,19 +1982,41 @@ class Table:
|
|
|
1811
1982
|
7. Handles exceptions by logging an error message and re-raising the exception.
|
|
1812
1983
|
8. Releases the lock regardless of success or failure.
|
|
1813
1984
|
|
|
1985
|
+
A schemaless table whose BSON file is about to be uploaded is compacted first, in this thread
|
|
1986
|
+
and under this lock (the compaction is a STRUCT publication, so it cannot run in the upload
|
|
1987
|
+
thread), and before the header, checksums and timestamp are prepared, so that the uploaded head
|
|
1988
|
+
and tail carry the moved pointers.
|
|
1989
|
+
|
|
1814
1990
|
Parameters:
|
|
1815
1991
|
force_write (bool): If True, forces the header to be written even if not modified.
|
|
1992
|
+
strict (bool): If True an upload or flush failure raises. It is always logged with the table name.
|
|
1816
1993
|
|
|
1817
1994
|
Raises:
|
|
1818
|
-
Exception: If the write or upload
|
|
1995
|
+
Exception: If the write fails (or, with strict, if the upload or the local flush fails).
|
|
1819
1996
|
"""
|
|
1820
1997
|
errmsg = ''
|
|
1821
1998
|
self.acquire()
|
|
1822
1999
|
try:
|
|
1823
2000
|
|
|
1824
2001
|
tini = time.time()
|
|
2002
|
+
from SharedData.services import s3_enabled
|
|
2003
|
+
remote = s3_enabled()
|
|
1825
2004
|
# create header
|
|
1826
2005
|
mtime = self.hdr['mtime']
|
|
2006
|
+
compacted = False
|
|
2007
|
+
if remote and self.is_schemaless and self.type == 1 and self.bsonpath.exists():
|
|
2008
|
+
remote_bson_mtime = S3GetMtime(str(self.bsonpath) + '.gzip', self.database_folder)
|
|
2009
|
+
if force_write or remote_bson_mtime is None or remote_bson_mtime < mtime:
|
|
2010
|
+
if self.compact_bson_if_worthwhile() is not None:
|
|
2011
|
+
compacted = True
|
|
2012
|
+
# New pointers: head, tail and BSON are all rewritten and all uploaded, under a
|
|
2013
|
+
# publication time later than anything the remote holds, so consumers see the change.
|
|
2014
|
+
newest = max([m for m in (remote_bson_mtime,
|
|
2015
|
+
S3GetMtime(str(self.headpath) + '.gzip', self.database_folder),
|
|
2016
|
+
S3GetMtime(str(self.tailpath) + '.gzip', self.database_folder))
|
|
2017
|
+
if m is not None] + [float(self.hdr['mtime'])])
|
|
2018
|
+
self.hdr['mtime'] = max(float(self.hdr['mtime']), newest + 0.001)
|
|
2019
|
+
mtime = self.hdr['mtime']
|
|
1827
2020
|
write_head = False
|
|
1828
2021
|
write_head = self.fill_header(force_write)
|
|
1829
2022
|
|
|
@@ -1833,21 +2026,32 @@ class Table:
|
|
|
1833
2026
|
if self.bson_file_handle is not None:
|
|
1834
2027
|
os.fsync(self.bson_file_handle.fileno())
|
|
1835
2028
|
|
|
1836
|
-
|
|
1837
|
-
if not s3_enabled():
|
|
2029
|
+
if not remote:
|
|
1838
2030
|
self.write_file()
|
|
1839
2031
|
return
|
|
1840
2032
|
|
|
2033
|
+
failures = []
|
|
2034
|
+
|
|
2035
|
+
def guarded(label, target, *args):
|
|
2036
|
+
# A thread's exception never reaches join(): keep it, name the table, let the caller decide.
|
|
2037
|
+
try:
|
|
2038
|
+
target(*args)
|
|
2039
|
+
except BaseException as error:
|
|
2040
|
+
failures.append('%s: %s' % (label, error))
|
|
2041
|
+
Logger.log.error('%s %s failed: %s' % (self.relpath, label, error))
|
|
2042
|
+
|
|
1841
2043
|
thread_s3 = threading.Thread(
|
|
1842
|
-
target=self.upload,
|
|
2044
|
+
target=guarded, args=('upload', self.upload, write_head, mtime, force_write or compacted, compacted))
|
|
1843
2045
|
thread_s3.start()
|
|
1844
2046
|
|
|
1845
|
-
thread_flush = threading.Thread(target=self.write_file)
|
|
2047
|
+
thread_flush = threading.Thread(target=guarded, args=('flush', self.write_file))
|
|
1846
2048
|
thread_flush.start()
|
|
1847
2049
|
|
|
1848
2050
|
# join threads
|
|
1849
2051
|
thread_s3.join()
|
|
1850
2052
|
thread_flush.join()
|
|
2053
|
+
if failures and strict:
|
|
2054
|
+
raise Exception('; '.join(failures))
|
|
1851
2055
|
|
|
1852
2056
|
te = time.time() - tini
|
|
1853
2057
|
datasize = self.hdr['count'] * self.hdr['itemsize'] / 1000000
|
|
@@ -2030,33 +2234,14 @@ class Table:
|
|
|
2030
2234
|
return write_head
|
|
2031
2235
|
|
|
2032
2236
|
############### UPLOAD ###############
|
|
2033
|
-
def upload(self, write_head, mtime, force_write=False):
|
|
2237
|
+
def upload(self, write_head, mtime, force_write=False, force_tail=False):
|
|
2034
2238
|
"""
|
|
2035
2239
|
Uploads the head, tail, and BSON files to remote storage based on modification times and flags.
|
|
2036
2240
|
|
|
2037
|
-
For schemaless tables
|
|
2038
|
-
|
|
2241
|
+
For schemaless tables the BSON file is uploaded after head and tail. Compaction is not done here:
|
|
2242
|
+
it is a structural publication and runs in write(), before the header is prepared.
|
|
2039
2243
|
"""
|
|
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
|
|
2244
|
+
# head/tail carry the pointers of the BSON uploaded below (compacted by write() when it was worth it)
|
|
2060
2245
|
remote_head_mtime = S3GetMtime(str(self.headpath)+'.gzip', self.database_folder)
|
|
2061
2246
|
remote_head_is_updated = False
|
|
2062
2247
|
if not remote_head_mtime is None:
|
|
@@ -2070,7 +2255,7 @@ class Table:
|
|
|
2070
2255
|
remote_tail_is_updated = False
|
|
2071
2256
|
if not remote_tail_mtime is None:
|
|
2072
2257
|
remote_tail_is_updated = remote_tail_mtime >= self.hdr['mtimetail']
|
|
2073
|
-
if not remote_tail_is_updated:
|
|
2258
|
+
if force_tail or not remote_tail_is_updated:
|
|
2074
2259
|
self.upload_tail(mtime)
|
|
2075
2260
|
else:
|
|
2076
2261
|
self.delete_tail()
|
|
@@ -2741,6 +2926,8 @@ class Table:
|
|
|
2741
2926
|
if (dirty or self._mapping_epoch != current
|
|
2742
2927
|
or self.size != int(self.hdr['recordssize'])):
|
|
2743
2928
|
self.attach()
|
|
2929
|
+
if getattr(self, 'is_schemaless', False):
|
|
2930
|
+
self._reset_bson_mapping() # the BSON file is published together with data.bin
|
|
2744
2931
|
if self.hasindex and not dirty:
|
|
2745
2932
|
self.index.rememmap()
|
|
2746
2933
|
self._mapping_epoch = current
|
|
@@ -134,6 +134,22 @@ def image(path):
|
|
|
134
134
|
sha256=digest.hexdigest())
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
def file_identity(path):
|
|
138
|
+
"""Cheap identity of a side file (BSON): where it lives and how big it is. Renames keep the inode."""
|
|
139
|
+
info = os.stat(path)
|
|
140
|
+
return dict(dev=info.st_dev, ino=info.st_ino, size=info.st_size)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def file_sha256(path):
|
|
144
|
+
digest = hashlib.sha256()
|
|
145
|
+
with open(path, 'rb') as stream:
|
|
146
|
+
while True:
|
|
147
|
+
chunk = stream.read(1 << 20)
|
|
148
|
+
if not chunk:
|
|
149
|
+
return digest.hexdigest()
|
|
150
|
+
digest.update(chunk)
|
|
151
|
+
|
|
152
|
+
|
|
137
153
|
def canonical(value):
|
|
138
154
|
return json.dumps(value, sort_keys=True, separators=(',', ':')).encode()
|
|
139
155
|
|
|
@@ -151,13 +167,24 @@ def _load(table):
|
|
|
151
167
|
descriptor = envelope['descriptor']
|
|
152
168
|
if hashlib.sha256(canonical(descriptor)).hexdigest() != envelope['sha256']:
|
|
153
169
|
raise ValueError('checksum mismatch')
|
|
154
|
-
if descriptor['version']
|
|
170
|
+
if descriptor['version'] not in (1, 2) or descriptor['magic'] != 'SharedData-record-rewrite':
|
|
155
171
|
raise ValueError('unsupported descriptor')
|
|
156
172
|
if descriptor['table'] != str(table.relpath).replace('\\', '/'):
|
|
157
173
|
raise ValueError('wrong table')
|
|
158
174
|
token = uuid.UUID(hex=descriptor['transaction']).hex
|
|
159
175
|
if descriptor['stage'] != 'data.bin.stage.' + token:
|
|
160
176
|
raise ValueError('invalid stage name')
|
|
177
|
+
if descriptor['version'] == 2: # a record image published together with its BSON side file
|
|
178
|
+
bson = descriptor['bson']
|
|
179
|
+
if bson['stage'] != 'bson.bin.stage.' + token:
|
|
180
|
+
raise ValueError('invalid BSON stage name')
|
|
181
|
+
for key in ('old', 'new'):
|
|
182
|
+
if not all(isinstance(bson[key][field], int) for field in ('dev', 'ino', 'size')):
|
|
183
|
+
raise ValueError('invalid BSON identity')
|
|
184
|
+
if not isinstance(bson['new']['sha256'], str) or len(bson['new']['sha256']) != 64:
|
|
185
|
+
raise ValueError('invalid BSON digest')
|
|
186
|
+
elif 'bson' in descriptor:
|
|
187
|
+
raise ValueError('BSON section requires version 2')
|
|
161
188
|
return descriptor
|
|
162
189
|
except Exception as error:
|
|
163
190
|
raise RewriteError('invalid pending record rewrite; files preserved') from error
|
|
@@ -173,6 +200,8 @@ def _finish(table, descriptor, recovery=False):
|
|
|
173
200
|
table.hasindex = bool(header['hasindex'])
|
|
174
201
|
table.attach()
|
|
175
202
|
table._index_bundle = None
|
|
203
|
+
if 'bson' in descriptor:
|
|
204
|
+
table._reset_bson_mapping() # the BSON file was replaced together with the records
|
|
176
205
|
checkpoint(prefix + 'data_attached')
|
|
177
206
|
table.hdr['isidxcreated'] = 0
|
|
178
207
|
sync_header(table)
|
|
@@ -188,6 +217,13 @@ def _finish(table, descriptor, recovery=False):
|
|
|
188
217
|
if image(stage) != descriptor['new']:
|
|
189
218
|
raise RewriteError('pending stage identity mismatch; files preserved')
|
|
190
219
|
stage.unlink()
|
|
220
|
+
if 'bson' in descriptor:
|
|
221
|
+
bson_stage = Path(table.filepath).parent / descriptor['bson']['stage']
|
|
222
|
+
if bson_stage.exists():
|
|
223
|
+
if file_identity(bson_stage)['size'] != descriptor['bson']['new']['size'] \
|
|
224
|
+
or file_sha256(bson_stage) != descriptor['bson']['new']['sha256']:
|
|
225
|
+
raise RewriteError('pending BSON stage identity mismatch; files preserved')
|
|
226
|
+
bson_stage.unlink()
|
|
191
227
|
pending_path(table).unlink()
|
|
192
228
|
checkpoint(prefix + 'descriptor_unlinked')
|
|
193
229
|
fsync_directory(Path(table.filepath).parent)
|
|
@@ -195,10 +231,44 @@ def _finish(table, descriptor, recovery=False):
|
|
|
195
231
|
table._rewrite_epoch_pending = True
|
|
196
232
|
|
|
197
233
|
|
|
234
|
+
def _bson_state(table, descriptor):
|
|
235
|
+
"""'old' or 'new' for the installed BSON file; anything else is refused, never guessed."""
|
|
236
|
+
bson = descriptor['bson']
|
|
237
|
+
try:
|
|
238
|
+
installed = file_identity(Path(table.filepath).parent / 'bson.bin')
|
|
239
|
+
except OSError as error:
|
|
240
|
+
raise RewriteError('missing authoritative BSON file; files preserved') from error
|
|
241
|
+
if (installed['dev'], installed['ino'], installed['size']) == (bson['old']['dev'], bson['old']['ino'], bson['old']['size']):
|
|
242
|
+
return 'old'
|
|
243
|
+
if installed['size'] == bson['new']['size'] and installed['ino'] == bson['new']['ino'] \
|
|
244
|
+
and file_sha256(Path(table.filepath).parent / 'bson.bin') == bson['new']['sha256']:
|
|
245
|
+
return 'new'
|
|
246
|
+
raise RewriteError('unknown authoritative BSON file; files preserved')
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _roll_forward_pair(table, descriptor, actual, stage):
|
|
250
|
+
"""Records and BSON are published as a pair: BSON first, then the records. A crash in between leaves
|
|
251
|
+
the new BSON next to the old records; the complete, validated stages are still there, so finish it.
|
|
252
|
+
(old, old): nothing was installed, the stages are discarded by _finish. (new, new): only cleanup is left.
|
|
253
|
+
(new records, old BSON) cannot arise from this order and is refused."""
|
|
254
|
+
folder = Path(table.filepath).parent
|
|
255
|
+
records = 'old' if actual == descriptor['old'] else 'new'
|
|
256
|
+
bson = _bson_state(table, descriptor)
|
|
257
|
+
if (records, bson) == ('new', 'old'):
|
|
258
|
+
raise RewriteError('unknown record/BSON pair state; files preserved')
|
|
259
|
+
if (records, bson) == ('old', 'new'):
|
|
260
|
+
if not stage.exists() or image(stage) != descriptor['new']:
|
|
261
|
+
raise RewriteError('missing record stage for the installed BSON file; files preserved')
|
|
262
|
+
checkpoint('recovery_pair_roll_forward')
|
|
263
|
+
os.replace(stage, table.filepath)
|
|
264
|
+
fsync_directory(folder)
|
|
265
|
+
checkpoint('recovery_pair_data_replaced')
|
|
266
|
+
|
|
267
|
+
|
|
198
268
|
def recover_rewrite(table):
|
|
199
269
|
"""Resolve old/new identity under pid lock, even after loss of all shm."""
|
|
200
|
-
if table.type != 1 or not pending_path(table).exists():
|
|
201
|
-
return False
|
|
270
|
+
if table.type != 1 or getattr(table, 'filepath', None) is None or not pending_path(table).exists():
|
|
271
|
+
return False # no path yet: __init__ failed before naming the file (free() acquires such a table)
|
|
202
272
|
require_rewrite_support()
|
|
203
273
|
if not table._owns_lock():
|
|
204
274
|
raise RewriteError('record recovery requires writer ownership')
|
|
@@ -214,6 +284,8 @@ def recover_rewrite(table):
|
|
|
214
284
|
stage = Path(table.filepath).parent / descriptor['stage']
|
|
215
285
|
if stage.exists() and image(stage) != descriptor['new']:
|
|
216
286
|
raise RewriteError('pending stage identity mismatch; files preserved')
|
|
287
|
+
if 'bson' in descriptor:
|
|
288
|
+
_roll_forward_pair(table, descriptor, actual, stage)
|
|
217
289
|
checkpoint('recovery_validated')
|
|
218
290
|
table._rewrite_recovering = True
|
|
219
291
|
try:
|
|
@@ -223,11 +295,17 @@ def recover_rewrite(table):
|
|
|
223
295
|
return True
|
|
224
296
|
|
|
225
297
|
|
|
226
|
-
def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_flag=None
|
|
298
|
+
def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_flag=None,
|
|
299
|
+
transform=None, header_updates=None, bson=None):
|
|
227
300
|
"""Copy selected committed rows to a complete file, then atomically publish.
|
|
228
301
|
|
|
229
302
|
`order` is a source-index vector, or None for the unchanged committed rows.
|
|
230
303
|
No record writes target the live mmap. Exceptions retain pending state.
|
|
304
|
+
|
|
305
|
+
transform(chunk, first_row) may edit a private copy of each chunk (for example to move BSON pointers),
|
|
306
|
+
header_updates is a dict of header fields for the replacement. `bson` = dict(token, stage, sha256, size)
|
|
307
|
+
names a complete, synced BSON replacement built beside bson.bin: both files are then published as a pair
|
|
308
|
+
(BSON first, records second, a version 2 descriptor makes the pair recoverable).
|
|
231
309
|
"""
|
|
232
310
|
require_rewrite_support()
|
|
233
311
|
if table.type != 1 or not table._owns_lock():
|
|
@@ -265,7 +343,9 @@ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_f
|
|
|
265
343
|
if sorted_flag is not None:
|
|
266
344
|
header['isidxsorted'] = sorted_flag
|
|
267
345
|
header['minchgid'] = min(int(header['minchgid']), start)
|
|
268
|
-
token = uuid.uuid4().hex
|
|
346
|
+
token = bson['token'] if bson else uuid.uuid4().hex
|
|
347
|
+
for field, value in (header_updates or {}).items():
|
|
348
|
+
header[field] = value
|
|
269
349
|
stage = Path(str(table.filepath) + '.stage.' + token)
|
|
270
350
|
prepare = Path(str(table.filepath) + '.rewrite.prepare.' + token)
|
|
271
351
|
source_stat = os.stat(table.filepath)
|
|
@@ -287,6 +367,8 @@ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_f
|
|
|
287
367
|
for offset in range(0, count, chunk_size):
|
|
288
368
|
end = min(count, offset + chunk_size)
|
|
289
369
|
chunk = rows[offset:end] if order is None else rows[order[offset:end]]
|
|
370
|
+
if transform is not None:
|
|
371
|
+
chunk = transform(np.array(chunk, copy=True), offset)
|
|
290
372
|
output = chunk.tobytes()
|
|
291
373
|
expected_hash.update(output)
|
|
292
374
|
write_all(fd, output)
|
|
@@ -302,10 +384,16 @@ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_f
|
|
|
302
384
|
if (new['sha256'] != expected_hash.hexdigest() or new['count'] != count
|
|
303
385
|
or new['capacity'] != capacity or image(table.filepath) != old):
|
|
304
386
|
raise RewriteError('replacement validation failed; live records unchanged')
|
|
305
|
-
descriptor = dict(magic='SharedData-record-rewrite', version=1,
|
|
387
|
+
descriptor = dict(magic='SharedData-record-rewrite', version=2 if bson else 1,
|
|
306
388
|
transaction=token, table=str(table.relpath).replace('\\', '/'),
|
|
307
389
|
operation=operation, start=int(start), stage=stage.name,
|
|
308
390
|
old=old, new=new)
|
|
391
|
+
if bson:
|
|
392
|
+
bson_stage = Path(table.bsonpath).parent / ('bson.bin.stage.' + token)
|
|
393
|
+
if not bson_stage.is_file() or bson_stage.stat().st_size != bson['size']:
|
|
394
|
+
raise RewriteError('BSON stage missing or of the wrong size; live files unchanged')
|
|
395
|
+
descriptor['bson'] = dict(stage=bson_stage.name, old=file_identity(table.bsonpath),
|
|
396
|
+
new=dict(file_identity(bson_stage), sha256=bson['sha256']))
|
|
309
397
|
envelope = dict(descriptor=descriptor, sha256=hashlib.sha256(canonical(descriptor)).hexdigest())
|
|
310
398
|
fd = os.open(prepare, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
311
399
|
try:
|
|
@@ -319,6 +407,11 @@ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_f
|
|
|
319
407
|
checkpoint('prepare_installed')
|
|
320
408
|
fsync_directory(stage.parent)
|
|
321
409
|
checkpoint('prepare_dir_synced')
|
|
410
|
+
if bson:
|
|
411
|
+
os.replace(bson_stage, table.bsonpath)
|
|
412
|
+
checkpoint('bson_replaced')
|
|
413
|
+
fsync_directory(stage.parent)
|
|
414
|
+
checkpoint('bson_dir_synced')
|
|
322
415
|
os.replace(stage, table.filepath)
|
|
323
416
|
checkpoint('data_replaced')
|
|
324
417
|
fsync_directory(stage.parent)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|