shareddata 6.85.10__tar.gz → 6.85.12__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.
Files changed (187) hide show
  1. {shareddata-6.85.10/src/shareddata.egg-info → shareddata-6.85.12}/PKG-INFO +1 -1
  2. {shareddata-6.85.10 → shareddata-6.85.12}/setup.py +1 -1
  3. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/CollectionMongoDB.py +1 -1
  4. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/SharedData.py +10 -9
  5. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/SharedNumpy.py +3 -2
  6. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Table.py +85 -9
  7. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableDisk.py +175 -4
  8. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableIndex.py +51 -15
  9. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableRead.py +8 -1
  10. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableRewrite.py +184 -4
  11. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TimeSeriesDisk.py +83 -1
  12. {shareddata-6.85.10 → shareddata-6.85.12/src/shareddata.egg-info}/PKG-INFO +1 -1
  13. {shareddata-6.85.10 → shareddata-6.85.12}/LICENSE +0 -0
  14. {shareddata-6.85.10 → shareddata-6.85.12}/MANIFEST.in +0 -0
  15. {shareddata-6.85.10 → shareddata-6.85.12}/README.md +0 -0
  16. {shareddata-6.85.10 → shareddata-6.85.12}/pyproject.toml +0 -0
  17. {shareddata-6.85.10 → shareddata-6.85.12}/setup.cfg +0 -0
  18. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/ServerGunicorn.py +0 -0
  19. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/ServerWaitress.py +0 -0
  20. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/__init__.py +0 -0
  21. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/apidocs-dark.css +0 -0
  22. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/apidocs.yml +0 -0
  23. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/auth.py +0 -0
  24. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/constants.py +0 -0
  25. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/download_paths.py +0 -0
  26. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/download_slots.py +0 -0
  27. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/install/worker.ps1 +0 -0
  28. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/install/worker.sh +0 -0
  29. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/listcache.py +0 -0
  30. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/__init__.py +0 -0
  31. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/cache.py +0 -0
  32. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/collections.py +0 -0
  33. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/data.py +0 -0
  34. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/downloads.py +0 -0
  35. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/metadata.py +0 -0
  36. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/operations.py +0 -0
  37. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/schedules.py +0 -0
  38. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/sqlite.py +0 -0
  39. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/system.py +0 -0
  40. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/tables.py +0 -0
  41. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/timeseries.py +0 -0
  42. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/web_routes.py +0 -0
  43. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/routes/workers.py +0 -0
  44. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/API/utils.py +0 -0
  45. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Auth/__init__.py +0 -0
  46. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Auth/bootstrap.py +0 -0
  47. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Auth/migrate.py +0 -0
  48. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Auth/store.py +0 -0
  49. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Auth/workers.py +0 -0
  50. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/CacheRedis.py +0 -0
  51. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/DataAPI/__init__.py +0 -0
  52. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/DataAPI/catalog.py +0 -0
  53. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/DataAPI/config.py +0 -0
  54. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/DataAPI/errors.py +0 -0
  55. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/DataAPI/indexes.py +0 -0
  56. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/DataAPI/readers.py +0 -0
  57. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/DataAPI/resolve.py +0 -0
  58. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/DataAPI/service.py +0 -0
  59. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Database.py +0 -0
  60. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Defaults.py +0 -0
  61. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/AWSEC2.py +0 -0
  62. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/AWSS3.py +0 -0
  63. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/AutoDocstrings.py +0 -0
  64. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/ClientAPI.py +0 -0
  65. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/ClientSocket.py +0 -0
  66. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/ClientWebSocket.py +0 -0
  67. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/LogHandlerAPI.py +0 -0
  68. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/LogReader.py +0 -0
  69. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/MongoDBClient.py +0 -0
  70. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/SaveTables.py +0 -0
  71. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/ServerSocket.py +0 -0
  72. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/ServerWebSocket.py +0 -0
  73. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/StreamsCache.py +0 -0
  74. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/StreamsPersist.py +0 -0
  75. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/SyncTable.py +0 -0
  76. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/TunnelWebSocket.py +0 -0
  77. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/IO/__init__.py +0 -0
  78. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Listing.py +0 -0
  79. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/LockWaiters.py +0 -0
  80. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Logger.py +0 -0
  81. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/MCP/__init__.py +0 -0
  82. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/MCP/__main__.py +0 -0
  83. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/MCP/client.py +0 -0
  84. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/MCP/server.py +0 -0
  85. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Metadata.py +0 -0
  86. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/MultiProc.py +0 -0
  87. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/OpenFIGI.py +0 -0
  88. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Redaction.py +0 -0
  89. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
  90. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/BatchJob.py +0 -0
  91. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/ContainerHealth.py +0 -0
  92. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/JobStore.py +0 -0
  93. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/RoutineCommand.py +0 -0
  94. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/RoutineOrigin.py +0 -0
  95. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/Schedule.py +0 -0
  96. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/ScheduleMirror.py +0 -0
  97. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
  98. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/ScheduleStore.py +0 -0
  99. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/Scheduler.py +0 -0
  100. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
  101. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/Worker.py +0 -0
  102. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
  103. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/WorkerLib.py +0 -0
  104. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/WorkerPool.py +0 -0
  105. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Routines/__init__.py +0 -0
  106. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/SQLite.py +0 -0
  107. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/StreamKafka.py +0 -0
  108. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Symbol.py +0 -0
  109. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableIndexJitFunctions.py +0 -0
  110. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
  111. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableIndexJitGenerate.py +0 -0
  112. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableIndexJitHash.py +0 -0
  113. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableIndexJitLoc.py +0 -0
  114. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TableProtocol.py +0 -0
  115. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/TimeseriesContainer.py +0 -0
  116. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Users.py +0 -0
  117. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Utils.py +0 -0
  118. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
  119. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/__init__.py +0 -0
  120. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/activity_logger.py +0 -0
  121. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/app.py +0 -0
  122. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
  123. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
  124. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/assets/google_logo.png +0 -0
  125. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/assets/kafka_keys.js +0 -0
  126. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/assets/logs.css +0 -0
  127. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/assets/sched.css +0 -0
  128. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/assets/styles.css +0 -0
  129. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/auth.py +0 -0
  130. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/backends.py +0 -0
  131. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/config.py +0 -0
  132. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/containers.py +0 -0
  133. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/identity.py +0 -0
  134. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/kafka_view.py +0 -0
  135. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/mongo_view.py +0 -0
  136. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/proc.py +0 -0
  137. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/redis_view.py +0 -0
  138. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/route_health.py +0 -0
  139. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/route_registry.py +0 -0
  140. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/sched.py +0 -0
  141. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/schedule_editor.py +0 -0
  142. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/server.py +0 -0
  143. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/services.py +0 -0
  144. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/shell.py +0 -0
  145. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/standalone.py +0 -0
  146. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/account.js +0 -0
  147. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/custom.css +0 -0
  148. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/favicon.ico +0 -0
  149. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/favicon.png +0 -0
  150. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/logo.png +0 -0
  151. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/logo_96x96.png +0 -0
  152. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
  153. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/shell.css +0 -0
  154. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/shell.js +0 -0
  155. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/standalone.css +0 -0
  156. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/static/standalone.js +0 -0
  157. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/suggest.py +0 -0
  158. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/account.html +0 -0
  159. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/base.html +0 -0
  160. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/claim.html +0 -0
  161. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/claim_done.html +0 -0
  162. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/dash.html +0 -0
  163. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/index.html +0 -0
  164. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/login.html +0 -0
  165. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/login_2fa.html +0 -0
  166. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/partials/navbar.html +0 -0
  167. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/partials/scripts.html +0 -0
  168. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/recovery_codes.html +0 -0
  169. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/shell.html +0 -0
  170. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/templates/standalone.html +0 -0
  171. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/twofactor.py +0 -0
  172. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/utils.py +0 -0
  173. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/Web/workerpool.py +0 -0
  174. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/__init__.py +0 -0
  175. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/build_info.py +0 -0
  176. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/fleet.py +0 -0
  177. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/preflight.py +0 -0
  178. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/private_files.py +0 -0
  179. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/release_backup.py +0 -0
  180. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/server.py +0 -0
  181. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/services.py +0 -0
  182. {shareddata-6.85.10 → shareddata-6.85.12}/src/SharedData/sharedmutexwin.pyd +0 -0
  183. {shareddata-6.85.10 → shareddata-6.85.12}/src/shareddata.egg-info/SOURCES.txt +0 -0
  184. {shareddata-6.85.10 → shareddata-6.85.12}/src/shareddata.egg-info/dependency_links.txt +0 -0
  185. {shareddata-6.85.10 → shareddata-6.85.12}/src/shareddata.egg-info/entry_points.txt +0 -0
  186. {shareddata-6.85.10 → shareddata-6.85.12}/src/shareddata.egg-info/requires.txt +0 -0
  187. {shareddata-6.85.10 → shareddata-6.85.12}/src/shareddata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.85.10
3
+ Version: 6.85.12
4
4
  Summary: Memory Mapped / Shared Memory Database with S3 repository
5
5
  Home-page: https://shareddata.net
6
6
  Author: shareddata
@@ -66,7 +66,7 @@ test_requires = [
66
66
 
67
67
  setup(
68
68
  name='shareddata',
69
- version='6.85.10',
69
+ version='6.85.12',
70
70
  description='Memory Mapped / Shared Memory Database with S3 repository',
71
71
  author='shareddata',
72
72
  author_email='admin@shareddata.net',
@@ -321,7 +321,7 @@ class CollectionMongoDB:
321
321
  # Insert all prepared documents into the collection
322
322
  result = []
323
323
  if documents_to_insert:
324
- result = self._collection.insert_many(documents_to_insert)
324
+ result = self._collection.insert_many(documents_to_insert, ordered=False)
325
325
 
326
326
  return result
327
327
 
@@ -6,6 +6,7 @@ import json
6
6
  import warnings
7
7
  import time
8
8
  import shutil
9
+ from SharedData.TableRewrite import remove_with_retry, rmtree_with_retry
9
10
  from datetime import datetime, timezone
10
11
 
11
12
  from multiprocessing import shared_memory
@@ -989,7 +990,7 @@ class SharedData:
989
990
  fpath = Path(os.environ['DATABASE_FOLDER'])
990
991
  fpath = fpath/('shm/'+shm_name.replace('\\', '/')+'.csv')
991
992
  if fpath.is_file():
992
- os.remove(fpath)
993
+ remove_with_retry(fpath)
993
994
  except:
994
995
  pass
995
996
 
@@ -1685,7 +1686,7 @@ class SharedData:
1685
1686
  except:
1686
1687
  try:
1687
1688
  if fpath.is_file():
1688
- os.remove(fpath)
1689
+ remove_with_retry(fpath)
1689
1690
  except:
1690
1691
  pass
1691
1692
  shm_names = shm_names.sort_index()
@@ -1845,11 +1846,11 @@ class SharedData:
1845
1846
  for file in delfiles:
1846
1847
  delpath = Path(localpath/file)
1847
1848
  if delpath.exists():
1848
- os.remove(delpath)
1849
+ remove_with_retry(delpath)
1849
1850
  # if folder is empty remove it
1850
1851
  if localpath.exists():
1851
1852
  if not any(localpath.iterdir()):
1852
- shutil.rmtree(localpath)
1853
+ rmtree_with_retry(localpath)
1853
1854
  else:
1854
1855
  database_folder = os.environ['DATABASE_FOLDER']
1855
1856
  # select the first disk with less percent_used
@@ -1865,10 +1866,10 @@ class SharedData:
1865
1866
  for file in delfiles:
1866
1867
  delpath = Path(localpath/file)
1867
1868
  if delpath.exists():
1868
- os.remove(delpath)
1869
+ remove_with_retry(delpath)
1869
1870
  # if folder is empty remove it
1870
1871
  if not any(localpath.iterdir()):
1871
- shutil.rmtree(localpath)
1872
+ rmtree_with_retry(localpath)
1872
1873
 
1873
1874
  from SharedData.services import s3_enabled
1874
1875
  if not localonly and s3_enabled():
@@ -1911,12 +1912,12 @@ class SharedData:
1911
1912
  del self.data[path]
1912
1913
  localpath = Path(os.environ['DATABASE_FOLDER'])/Path(path)
1913
1914
  if localpath.exists():
1914
- shutil.rmtree(localpath)
1915
+ rmtree_with_retry(localpath)
1915
1916
  files_del = ['timeseries_head.bin','timeseries_tail.bin']
1916
1917
  for file in files_del:
1917
1918
  delpath = Path(localpath.parent/file)
1918
1919
  if delpath.exists():
1919
- os.remove(delpath)
1920
+ remove_with_retry(delpath)
1920
1921
  from SharedData.services import s3_enabled
1921
1922
  if not localonly and s3_enabled():
1922
1923
  S3DeleteTimeseries(path.replace('/timeseries',''))
@@ -1928,7 +1929,7 @@ class SharedData:
1928
1929
  fpath, shm_name = tstag.get_path()
1929
1930
  del self.data[path].tags[tag]
1930
1931
  del ts
1931
- os.remove(fpath)
1932
+ remove_with_retry(fpath)
1932
1933
  return True
1933
1934
 
1934
1935
  except Exception as e:
@@ -297,8 +297,9 @@ class SharedNumpy(np.ndarray):
297
297
  force_write (bool): If True, forces the write operation even if not necessary. Defaults to False.
298
298
  """
299
299
  # Saving remains available without unsupported record replacement.
300
- if (self.table.type != 1 or sys.platform == 'linux') and (
301
- self.table.hdr['hasindex'] == 1 and self.table.hdr['isidxsorted'] == 0):
300
+ from SharedData.TableRewrite import rewrite_supported
301
+ if (self.table.type != 1 or sys.platform == 'linux' or not rewrite_supported()) and (
302
+ self.table.hdr['hasindex'] == 1 and self.table.hdr['isidxsorted'] == 0): # Windows sorts in place
302
303
  self.sort_index()
303
304
  self.table.write(force_write, strict)
304
305
 
@@ -687,8 +687,29 @@ class Table:
687
687
  records, so they are removed and rebuilt from the new empty header.
688
688
  """
689
689
  from SharedData.TableRewrite import (RewriteError, fsync_directory,
690
- require_rewrite_support)
691
- require_rewrite_support()
690
+ rewrite_supported)
691
+ if not rewrite_supported():
692
+ # Windows cannot rename over or unlink a file any process maps. The shared mutex is held in STRUCT
693
+ # mode, so the image is re-initialised in place: the old committed rows and the header are zeroed
694
+ # through a mapped view (coherent with every other process's view), the file is only ever extended,
695
+ # and the index / BSON files stay as they are (the index is rebuilt by the caller, and the new
696
+ # header resets next_bson_ptr, so no old document is reachable).
697
+ from SharedData.TableRewrite import RewriteError, extend_file, read_layout
698
+ final = self.filepath
699
+ try:
700
+ with open(final, 'rb') as stream:
701
+ old_header, old_records = read_layout(stream)
702
+ clear = old_header.dtype.itemsize + int(old_header['count']) * old_records.itemsize
703
+ except RewriteError:
704
+ clear = os.path.getsize(final) # not a coherent image: clear everything it holds
705
+ extend_file(final, int(self.hdrdtype.itemsize + self.recdtype.itemsize * int(self.size)))
706
+ clear = max(clear, int(self.hdrdtype.itemsize))
707
+ view = np.memmap(final, np.uint8, 'r+', 0, (clear,))
708
+ view[:] = 0
709
+ view.flush()
710
+ del view
711
+ self.malloc(create=True)
712
+ return
692
713
  final = self.filepath
693
714
  old = os.stat(final)
694
715
  # The writer lock is held: any earlier stage is from a build that died.
@@ -1385,7 +1406,7 @@ class Table:
1385
1406
  return h1[:l] == h2[:l]
1386
1407
 
1387
1408
  ############### BSON MANAGEMENT ###############
1388
- def _reset_bson_mapping(self):
1409
+ def _reset_bson_mapping(self, reopen=True):
1389
1410
  """Drop the BSON mapping and reopen the file by path: after a paired publication the old
1390
1411
  mapping belongs to an unlinked inode and its offsets no longer match the records."""
1391
1412
  if self.bson_mmap is not None:
@@ -1399,7 +1420,7 @@ class Table:
1399
1420
  self.bson_file_handle = None
1400
1421
  self.curr_bson_size = 0
1401
1422
  self.prev_bson_ptr = 0
1402
- if self.is_schemaless and self.bsonpath.exists() and self.bsonpath.stat().st_size > 0:
1423
+ if reopen and self.is_schemaless and self.bsonpath.exists() and self.bsonpath.stat().st_size > 0:
1403
1424
  self.init_bson_mmap()
1404
1425
 
1405
1426
  def init_bson_mmap(self, from_download=False):
@@ -1668,6 +1689,50 @@ class Table:
1668
1689
  return None
1669
1690
  return self.defragment_bson(acquire=False)
1670
1691
 
1692
+ def _defrag_in_place(self, stage, mapping, payload, new_size):
1693
+ """Windows: the compacted BSON `stage` is copied over the start of the live file and every moved
1694
+ bson_ptr is rewritten in place, as one STRUCT section under the shared mutex (nothing else touches the
1695
+ table), then the file is shrunk when no process maps it. Not crash-safe (a crash between the BSON
1696
+ copy and the pointer update leaves STRUCT/DIRTY and pointers into a rewritten file); the staged
1697
+ publication that covers that needs Linux."""
1698
+ from SharedData.TableProtocol import structure
1699
+ from SharedData.TableRewrite import shrink_file, sync_header
1700
+ count = int(self.hdr['count'])
1701
+ with structure(self):
1702
+ if self.bson_mmap is not None:
1703
+ self.bson_mmap.flush()
1704
+ data = np.fromfile(stage, dtype=np.uint8) # the stage is the compacted payload (+1 pad byte)
1705
+ view = np.memmap(self.bsonpath, np.uint8, 'r+', 0, (len(data),))
1706
+ view[:] = data
1707
+ view.flush()
1708
+ del view
1709
+ rows = self.shf_data[:count] # the file's own mapping, never a caller-visible copy
1710
+ selected = mapping >= 0
1711
+ if selected.any():
1712
+ column = rows['bson_ptr']
1713
+ if column.dtype.kind == 'V' and column.dtype.itemsize == 8:
1714
+ column = column.view(np.int64)
1715
+ column[selected] = mapping[selected]
1716
+ self.shf_data.flush()
1717
+ self.hdr['next_bson_ptr'] = payload
1718
+ self.hdr['mtime'] = max(datetime.now().timestamp(), float(self.hdr['mtime']) + 0.001)
1719
+ sync_header(self)
1720
+ self.attach() # records/header of this object: the mapping of the file, as after a publication
1721
+ self._reset_bson_mapping(reopen=False) # drop this process's map, then try to give the space back
1722
+ physical = self.bsonpath.stat().st_size
1723
+ if not shrink_file(self.bsonpath, new_size) and physical > new_size:
1724
+ # another process still maps the file: it stays as large, but the stale documents behind the
1725
+ # new end are zeroed so the upload (which compresses the file to its end) stays small
1726
+ tail = np.memmap(self.bsonpath, np.uint8, 'r+', new_size, (physical - new_size,))
1727
+ tail[:] = 0
1728
+ tail.flush()
1729
+ del tail
1730
+ self._reset_bson_mapping()
1731
+ try:
1732
+ Path(stage).unlink()
1733
+ except OSError:
1734
+ pass
1735
+
1671
1736
  def _defragment_bson_disk(self, chunk_size, acquire):
1672
1737
  """Compact bson.bin of a disk table and move every bson_ptr, as ONE crash-safe publication.
1673
1738
 
@@ -1681,7 +1746,8 @@ class Table:
1681
1746
  from SharedData.TableRewrite import (RewriteError, copy_metadata, file_identity, fsync_directory,
1682
1747
  pending_path, publish_rewrite, require_rewrite_support, write_all)
1683
1748
  from SharedData.TableProtocol import structure
1684
- require_rewrite_support()
1749
+ from SharedData.TableRewrite import rewrite_supported
1750
+ in_place = not rewrite_supported()
1685
1751
  if not self.is_schemaless:
1686
1752
  raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
1687
1753
  if acquire:
@@ -1695,8 +1761,8 @@ class Table:
1695
1761
  if self.bson_mmap is not None:
1696
1762
  self.bson_mmap.flush()
1697
1763
  if self.bsonpath.exists():
1698
- with open(self.bsonpath, 'rb') as baseline:
1699
- os.fsync(baseline.fileno())
1764
+ from SharedData.TableRewrite import sync_file
1765
+ sync_file(self.bsonpath) # r+b on Windows: fsync needs a writable handle
1700
1766
  count = int(self.hdr['count'])
1701
1767
  pointers, sizes = self._bson_columns(count) if count else (np.zeros(0, np.int64), np.zeros(0, np.int64))
1702
1768
  live_idx = np.flatnonzero(sizes > 0)
@@ -1722,9 +1788,10 @@ class Table:
1722
1788
  token = uuid.uuid4().hex
1723
1789
  stage = self.bsonpath.parent / ('bson.bin.stage.' + token)
1724
1790
  digest = hashlib.sha256()
1725
- fd = os.open(stage, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
1791
+ fd = os.open(stage, os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0), 0o600)
1726
1792
  try:
1727
- copy_metadata(self.bsonpath, fd)
1793
+ if not in_place:
1794
+ copy_metadata(self.bsonpath, fd)
1728
1795
  if len(live_idx):
1729
1796
  with open(self.bsonpath, 'rb') as source:
1730
1797
  view = mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ)
@@ -1751,6 +1818,15 @@ class Table:
1751
1818
  new_size = max(payload, 1)
1752
1819
  mapping = np.full(count, -1, dtype=np.int64)
1753
1820
  mapping[live_idx] = new_offsets
1821
+ if in_place:
1822
+ self._defrag_in_place(stage, mapping, payload, new_size)
1823
+ stats = {'original_size': original_size, 'new_size': self.bsonpath.stat().st_size,
1824
+ 'reclaimed': original_size - self.bsonpath.stat().st_size, 'live_bytes': payload,
1825
+ 'live_rows': int(len(live_idx)), 'logical_size': new_size}
1826
+ Logger.log.info(
1827
+ f'Defragmented {self.relpath} BSON file in place: {original_size/1024/1024:.2f}MB -> '
1828
+ f'{new_size/1024/1024:.2f}MB logical (file {stats["new_size"]/1024/1024:.2f}MB)')
1829
+ return stats
1754
1830
 
1755
1831
  def move_pointers(chunk, first):
1756
1832
  moved = mapping[first:first + len(chunk)]
@@ -13,6 +13,7 @@ from tqdm import tqdm
13
13
  import threading
14
14
  import psutil
15
15
  import shutil
16
+ from SharedData.TableRewrite import remove_with_retry, rmtree_with_retry
16
17
 
17
18
  from SharedData.Logger import Logger
18
19
  from SharedData.IO.AWSS3 import S3DeleteFolder
@@ -178,6 +179,9 @@ class TableDisk(Table):
178
179
  # Header, size check and data come from one open file: trim replaces
179
180
  # data.bin, and np.memmap by path would map (and extend) the new file.
180
181
  with open(self.filepath, 'r+b') as stream:
182
+ from SharedData.TableRewrite import rewrite_supported
183
+ if not rewrite_supported():
184
+ self._adopt_layout(stream) # Windows: layout changes are published in place
181
185
  # memory map header — read the live header, never write it
182
186
  self.shf_hdr = np.memmap(stream, self.hdrdtype, 'r+', 0, (1,))
183
187
  self.hdr = self.shf_hdr[0]
@@ -199,6 +203,24 @@ class TableDisk(Table):
199
203
  self.records = SharedNumpy('DISK', self.shf_data)
200
204
  self.records.table = self
201
205
 
206
+ def _adopt_layout(self, stream):
207
+ """Follow a layout change published under the same inode (overwrite or reload with another schema,
208
+ in place): the header describes itself, so a process that still holds the old column list would map
209
+ the new bytes with the old record stride. Reparsed from the file, never from this object's copy."""
210
+ from SharedData.TableRewrite import RewriteError, read_layout
211
+ try:
212
+ header, records = read_layout(stream)
213
+ except RewriteError:
214
+ return # not a coherent image (yet): the caller's own checks report it
215
+ if header.dtype == self.hdrdtype and records == self.recdtype:
216
+ return
217
+ self.hdrdtype, self.recdtype = header.dtype, records
218
+ self.recnames, self.recformats = list(records.names), [str(records[n]) for n in records.names]
219
+ self.is_schemaless = 'bson_size' in self.recnames and 'bson_ptr' in self.recnames
220
+ self._record_bundle = None
221
+ self._index_bundle = None
222
+ self._reset_bson_mapping()
223
+
202
224
  ############### FREE ###############
203
225
  def free(self, acquire=True):
204
226
  """
@@ -300,7 +322,12 @@ class TableDisk(Table):
300
322
  """
301
323
  if self.hdr['recordssize'] <= self.hdr['count']:
302
324
  return self
303
- from SharedData.TableRewrite import publish_rewrite
325
+ from SharedData.TableRewrite import publish_rewrite, rewrite_supported
326
+ if not rewrite_supported():
327
+ # Shrinking needs a second file replacing the mapped one (Linux). Trim only returns unused space,
328
+ # so the table is left as it is rather than failing.
329
+ Logger.log.warning('%s not trimmed: shrinking a disk table needs POSIX file APIs' % self.relpath)
330
+ return self
304
331
  return publish_rewrite(self, None, 'trim', capacity=max(2, int(self.hdr['count'])))
305
332
 
306
333
  ############### RELOAD ###############
@@ -342,6 +369,9 @@ class TableDisk(Table):
342
369
  return self.records
343
370
  if change == 'tail' and not self.is_schemaless:
344
371
  return self._reload_tail()
372
+ from SharedData.TableRewrite import rewrite_supported
373
+ if not rewrite_supported():
374
+ return self._reload_image_in_place()
345
375
  require_rewrite_support()
346
376
  final = self.filepath
347
377
  old = os.stat(final)
@@ -396,6 +426,144 @@ class TableDisk(Table):
396
426
  raise Exception(errmsg)
397
427
  return self.records
398
428
 
429
+ def _apply_tail_in_place(self, header, tail_io, tailhdr, tail_at, capacity):
430
+ """Windows: the remote tail (already parsed up to its rows) is downloaded and checked against its
431
+ header and checksum into a private temporary file first; only then, in a STRUCT section under the
432
+ shared mutex, is the live file extended if the capacity grew, the tail rows are written through a
433
+ mapped view, rows beyond a shorter tail are zeroed, the header fields are published and the indexes
434
+ rebuilt. A failure before the STRUCT section leaves the live table untouched."""
435
+ import hashlib
436
+ import uuid
437
+ from SharedData.TableRewrite import RewriteError, extend_file, same_digest, sync_header, zero_mapped, copy_mapped
438
+ itemsize, tailsize = int(header['itemsize']), int(tailhdr['tailsize'])
439
+ final = self.filepath
440
+ temp = final.parent / ('data.bin.tail.' + uuid.uuid4().hex)
441
+ try:
442
+ tail_io.seek(tailhdr.dtype.itemsize)
443
+ digest, written = hashlib.md5(), 0
444
+ with open(temp, 'wb') as out:
445
+ while True:
446
+ chunk = tail_io.read(CHUNK)
447
+ if not chunk:
448
+ break
449
+ if written + len(chunk) > tailsize * itemsize:
450
+ raise RewriteError('the remote tail of %s is longer than its header says' % self.relpath)
451
+ digest.update(chunk)
452
+ out.write(chunk)
453
+ written += len(chunk)
454
+ out.flush()
455
+ os.fsync(out.fileno())
456
+ if written != tailsize * itemsize:
457
+ raise RewriteError('the remote tail of %s holds %d bytes, its header says %d' % (self.relpath, written, tailsize * itemsize))
458
+ if not same_digest(digest.digest(), tailhdr['md5hash']):
459
+ raise RewriteError('the remote tail of %s does not match its checksum' % self.relpath)
460
+ with structure(self):
461
+ hdrsize = int(self.hdr.dtype.itemsize)
462
+ old_end = hdrsize + int(self.hdr['count']) * itemsize
463
+ new_end = tail_at + written
464
+ extend_file(final, hdrsize + capacity * itemsize)
465
+ self.shf_data.flush()
466
+ copy_mapped(final, temp, written, dst_offset=tail_at)
467
+ zero_mapped(final, new_end, old_end) # a shorter tail than the local one leaves no old rows behind
468
+ for field in ('md5hashtail', 'tailsize', 'count', 'mtimetail', 'recordssize', 'isidxcreated',
469
+ 'isloaded', 'isidxsorted', 'minchgid'):
470
+ self.hdr[field] = header[field]
471
+ sync_header(self)
472
+ self.size = capacity
473
+ self._attach_replacement(final, rebuild_files=False)
474
+ return self.records
475
+ finally:
476
+ try:
477
+ temp.unlink()
478
+ except OSError:
479
+ pass
480
+
481
+ def _reload_image_in_place(self):
482
+ """Windows: the whole remote image is downloaded beside the live files (the live BSON path is
483
+ redirected too, so nothing live is written while preparing), then, inside the STRUCT section the
484
+ caller holds, copied over the live files through mapped views: the file is extended when the remote
485
+ capacity is larger, committed rows left over from a longer local image are zeroed, the BSON file is
486
+ copied and shrunk when no other process maps it. Index files are kept (rebuilt, oversized is fine)."""
487
+ import gc
488
+ import uuid
489
+ from SharedData.TableRewrite import (RewriteError, copy_mapped, extend_file, read_layout, shrink_file,
490
+ zero_mapped)
491
+ final, bson_final = self.filepath, self.bsonpath
492
+ for leftover in list(final.parent.glob('data.bin.reload.*')) + list(final.parent.glob('bson.bin.reload.*')):
493
+ try:
494
+ leftover.unlink()
495
+ except OSError:
496
+ pass
497
+ token = uuid.uuid4().hex
498
+ stage = final.parent / ('data.bin.reload.' + token)
499
+ bson_stage = final.parent / ('bson.bin.reload.' + token)
500
+ old_end = int(self.hdr.dtype.itemsize) + int(self.hdr['count']) * int(self.recdtype.itemsize)
501
+ old_bson = bson_final.stat().st_size if bson_final.exists() else 0
502
+ old_layout = (self.hdrdtype, self.recdtype, getattr(self, 'recnames', None), getattr(self, 'recformats', None),
503
+ self.is_schemaless)
504
+ saved = (self.hdr, self.size, getattr(self, 'orig_hdr', None))
505
+ self.filepath, self.bsonpath, self.hdr = stage, bson_stage, None
506
+ self._bootstrap_record_download = True
507
+ try:
508
+ self._download_empty_image()
509
+ if self.hdr is None or not stage.is_file():
510
+ raise Exception('the remote copy of %s disappeared during the reload' % self.relpath)
511
+ self.free(acquire=False) # the maps of the stage files
512
+ gc.collect()
513
+ with open(stage, 'rb') as stream:
514
+ downloaded, records = read_layout(stream)
515
+ hdrsize = int(downloaded.dtype.itemsize)
516
+ commit = hdrsize + int(downloaded['count']) * int(records.itemsize)
517
+ capacity_bytes = hdrsize + int(downloaded['recordssize']) * int(records.itemsize)
518
+ new_hdrdtype, new_recdtype, new_size = self.hdrdtype, self.recdtype, self.size
519
+ new_schemaless = self.is_schemaless
520
+ have_bson = bson_stage.exists()
521
+ self.filepath, self.bsonpath = final, bson_final
522
+ self._reset_bson_mapping(reopen=False)
523
+ extend_file(final, capacity_bytes)
524
+ copy_mapped(final, stage, commit)
525
+ zero_mapped(final, commit, old_end)
526
+ if have_bson:
527
+ bson_size = bson_stage.stat().st_size
528
+ if not bson_final.exists():
529
+ bson_final.touch()
530
+ extend_file(bson_final, bson_size)
531
+ copy_mapped(bson_final, bson_stage, bson_size)
532
+ physical = bson_final.stat().st_size
533
+ if not shrink_file(bson_final, bson_size) and physical > bson_size:
534
+ zero_mapped(bson_final, bson_size, physical)
535
+ except BaseException:
536
+ self.filepath, self.bsonpath = final, bson_final
537
+ self.hdr, self.size, self.orig_hdr = saved
538
+ self.hdrdtype, self.recdtype = old_layout[0], old_layout[1]
539
+ if old_layout[2] is not None:
540
+ self.recnames, self.recformats = old_layout[2], old_layout[3]
541
+ self.is_schemaless = old_layout[4]
542
+ raise
543
+ finally:
544
+ self._bootstrap_record_download = False
545
+ for leftover in (stage, bson_stage):
546
+ try:
547
+ leftover.unlink()
548
+ except OSError:
549
+ pass
550
+ try:
551
+ self.free(acquire=False)
552
+ self.malloc()
553
+ if self.is_schemaless:
554
+ self.init_bson_mmap(from_download=have_bson)
555
+ if self.hasindex:
556
+ self.hdr['isidxcreated'] = 0
557
+ self.index.initialize()
558
+ self.shareddata.data[self.relpath] = self
559
+ self.hdr['isloaded'] = 1
560
+ self.mutex['isloaded'] = 1
561
+ except Exception as e:
562
+ errmsg = f"TableDisk.reload() {self.relpath}: {e}"
563
+ Logger.log.error(errmsg)
564
+ raise Exception(errmsg)
565
+ return self.records
566
+
399
567
  def _attach_replacement(self, final, rebuild_files):
400
568
  """After os.replace: make the directory entry durable, then map the new image and rebuild its
401
569
  indexes. Whatever fails, this object ends mapped to the image that is now installed (flagged
@@ -449,7 +617,8 @@ class TableDisk(Table):
449
617
  from SharedData.TableRewrite import (RewriteError, copy_metadata, pwrite_all, require_rewrite_support,
450
618
  same_digest, sync_header)
451
619
  import hashlib
452
- require_rewrite_support()
620
+ from SharedData.TableRewrite import rewrite_supported
621
+ in_place = not rewrite_supported()
453
622
  acquired = not self._owns_lock()
454
623
  if acquired:
455
624
  self.acquire()
@@ -484,6 +653,8 @@ class TableDisk(Table):
484
653
  header['isidxsorted'] = 1 # the uploader sorts before it writes (SharedNumpy.write)
485
654
  pending = int(header['minchgid']) # a head row edited here and not uploaded yet stays marked for upload;
486
655
  header['minchgid'] = pending if pending <= int(header['headsize']) else int(header['count']) + 1 # otherwise as a write leaves it
656
+ if in_place:
657
+ return self._apply_tail_in_place(header, tail_io, tailhdr, tail_at, capacity)
487
658
  for leftover in final.parent.glob('data.bin.reload.*'): # the lock is held: a stage left by a dead reload
488
659
  leftover.unlink()
489
660
  stage = final.parent / ('data.bin.reload.' + uuid.uuid4().hex)
@@ -569,10 +740,10 @@ class TableDisk(Table):
569
740
  for file in delfiles:
570
741
  delpath = Path(localpath/file)
571
742
  if delpath.exists():
572
- os.remove(delpath)
743
+ remove_with_retry(delpath)
573
744
  # if folder is empty remove it
574
745
  if not any(localpath.iterdir()):
575
- shutil.rmtree(localpath)
746
+ rmtree_with_retry(localpath)
576
747
 
577
748
  success = True
578
749
 
@@ -257,7 +257,7 @@ class TableIndex:
257
257
  resetfile = False
258
258
  if (not os.path.exists(self.pkeypath)):
259
259
  resetfile = True
260
- elif os.path.getsize(self.pkeypath) != keysize_bytes:
260
+ elif not self._index_file_fits(self.pkeypath, keysize_bytes):
261
261
  resetfile = True
262
262
 
263
263
  if resetfile:
@@ -307,7 +307,7 @@ class TableIndex:
307
307
  resetfile = False
308
308
  if (not os.path.exists(self.dateidxpath)):
309
309
  resetfile = True
310
- elif os.path.getsize(self.dateidxpath) != size_bytes:
310
+ elif not self._index_file_fits(self.dateidxpath, size_bytes):
311
311
  resetfile = True
312
312
 
313
313
  if resetfile:
@@ -365,7 +365,7 @@ class TableIndex:
365
365
  resetfile = False
366
366
  if (not os.path.exists(self.symbolidxpath)):
367
367
  resetfile = True
368
- elif os.path.getsize(self.symbolidxpath) != size_bytes:
368
+ elif not self._index_file_fits(self.symbolidxpath, size_bytes):
369
369
  resetfile = True
370
370
 
371
371
  if resetfile:
@@ -425,7 +425,7 @@ class TableIndex:
425
425
  resetfile = False
426
426
  if (not os.path.exists(self.portidxpath)):
427
427
  resetfile = True
428
- elif os.path.getsize(self.portidxpath) != size_bytes:
428
+ elif not self._index_file_fits(self.portidxpath, size_bytes):
429
429
  resetfile = True
430
430
  if resetfile:
431
431
  self.create_file(self.portidxpath,size_bytes)
@@ -618,14 +618,32 @@ class TableIndex:
618
618
  Logger.log.error('Error sorting index!\n%s' % (e))
619
619
  raise
620
620
 
621
+ @staticmethod
622
+ def _inplace_files():
623
+ """True where an index file cannot be replaced while mapped (Windows): it is extended instead."""
624
+ from SharedData.TableRewrite import rewrite_supported
625
+ return not rewrite_supported()
626
+
627
+ def _stream_fits(self, stream, size_bytes):
628
+ size = os.fstat(stream.fileno()).st_size
629
+ return size == size_bytes or (size > size_bytes and self._inplace_files())
630
+
631
+ def _index_file_fits(self, path, size_bytes):
632
+ """The file can back an index of `size_bytes`: exactly that size, or (in-place platforms) larger.
633
+
634
+ Maps are always created with an explicit shape, so an oversized file is harmless; shrinking one
635
+ would need every process to drop its mapping first, which Windows enforces and cannot be asked for."""
636
+ size = os.path.getsize(path)
637
+ return size == size_bytes or (size > size_bytes and self._inplace_files())
638
+
621
639
  def create_file(self,fpath,size):
622
640
  """
623
641
  Creates a file at the specified path with the given size.
624
642
 
625
- This method opens a file in binary write mode, seeks to the byte position size-1,
626
- and writes a single null byte to allocate the file space. On POSIX systems, it uses
627
- os.posix_fallocate to efficiently allocate disk space. On Windows systems, the allocation
628
- step is currently not implemented.
643
+ Linux: a complete replacement file is built beside the target, synced, and installed with one
644
+ os.replace (os.posix_fallocate reserves the space). Windows cannot rename over a mapped file, so an
645
+ existing file is only ever extended in place (extension is allowed with live mappings; the arrays
646
+ are re-initialised by the caller).
629
647
 
630
648
  Parameters:
631
649
  fpath (str): The file path where the file will be created.
@@ -634,6 +652,19 @@ class TableIndex:
634
652
  Raises:
635
653
  OSError: If file operations or allocation fail.
636
654
  """
655
+ from SharedData.TableRewrite import checkpoint, extend_file, fsync_directory, sync_header
656
+ if self._inplace_files():
657
+ if os.path.exists(fpath):
658
+ extend_file(fpath, size)
659
+ else:
660
+ with open(fpath, 'wb') as f:
661
+ f.seek(size-1)
662
+ f.write(b'\x00')
663
+ f.flush()
664
+ os.fsync(f.fileno())
665
+ self.table.hdr['isidxcreated'] = 0
666
+ sync_header(self.table)
667
+ return
637
668
  temporary = str(fpath) + '.tmp.' + uuid.uuid4().hex
638
669
  with open(temporary, 'wb') as f:
639
670
  # Seek to the end of the file
@@ -642,11 +673,8 @@ class TableIndex:
642
673
  f.write(b'\x00')
643
674
  if os.name == 'posix':
644
675
  os.posix_fallocate(f.fileno(), 0, size)
645
- elif os.name == 'nt':
646
- pass # TODO: implement windows file allocation
647
676
  f.flush()
648
677
  os.fsync(f.fileno())
649
- from SharedData.TableRewrite import checkpoint, fsync_directory, sync_header
650
678
  self.table.hdr['isidxcreated'] = 0
651
679
  sync_header(self.table)
652
680
  checkpoint('before_index_replace_' + os.path.basename(fpath))
@@ -689,7 +717,15 @@ class TableIndex:
689
717
  # smaller file to the size checked: a lock-free reader resized the index.
690
718
  # Infer keysize from actual file size
691
719
  with open(self.pkeypath, 'r+b') as stream:
692
- keysize = os.fstat(stream.fileno()).st_size // 8 # int64 = 8 bytes
720
+ filesize = os.fstat(stream.fileno()).st_size
721
+ if self._inplace_files():
722
+ # the file may be larger than the table needs (it is never shrunk): the hash modulus is
723
+ # the table's, not the file's
724
+ keysize = int(self.table.size * 3)
725
+ if filesize < keysize * 8:
726
+ raise Exception(f'Primary key index file is smaller than expected: {self.pkeypath}')
727
+ else:
728
+ keysize = filesize // 8 # int64 = 8 bytes
693
729
  self.pkey = np.memmap(stream, np.int64, 'r+', 0, (keysize,))
694
730
 
695
731
  # Remap date indices if present
@@ -707,7 +743,7 @@ class TableIndex:
707
743
 
708
744
  # Check if file size matches
709
745
  with open(self.dateidxpath, 'r+b') as stream:
710
- if os.fstat(stream.fileno()).st_size == expected_size:
746
+ if self._stream_fits(stream, expected_size):
711
747
  self.datelastidx = np.memmap(stream, np.int64, 'r+', 0, (dateidxsize,))
712
748
  self.dateprevidx = np.memmap(stream, np.int64, 'r+', hashtblsize_bytes, (listsize,))
713
749
  else:
@@ -726,7 +762,7 @@ class TableIndex:
726
762
 
727
763
  # Check if file size matches
728
764
  with open(self.symbolidxpath, 'r+b') as stream:
729
- if os.fstat(stream.fileno()).st_size == expected_size:
765
+ if self._stream_fits(stream, expected_size):
730
766
  self.symbollastidx = np.memmap(stream, np.int64, 'r+', 0, (hashtblsize,))
731
767
  self.symbolprevidx = np.memmap(stream, np.int64, 'r+', hashtblsize_bytes, (listsize,))
732
768
  else:
@@ -745,7 +781,7 @@ class TableIndex:
745
781
 
746
782
  # Check if file size matches
747
783
  with open(self.portidxpath, 'r+b') as stream:
748
- if os.fstat(stream.fileno()).st_size == expected_size:
784
+ if self._stream_fits(stream, expected_size):
749
785
  self.portlastidx = np.memmap(stream, np.int64, 'r+', 0, (hashtblsize,))
750
786
  self.portprevidx = np.memmap(stream, np.int64, 'r+', hashtblsize_bytes, (listsize,))
751
787
  else: