shareddata 6.85.0__tar.gz → 6.85.1__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.0/src/shareddata.egg-info → shareddata-6.85.1}/PKG-INFO +1 -1
- {shareddata-6.85.0 → shareddata-6.85.1}/setup.py +1 -1
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/apidocs.yml +1 -1
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/resolve.py +11 -3
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Redaction.py +29 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Table.py +55 -3
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/config.py +1 -0
- shareddata-6.85.1/src/SharedData/Web/proc.py +170 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/standalone.py +31 -1
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/standalone.css +13 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/standalone.js +132 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/standalone.html +7 -0
- {shareddata-6.85.0 → shareddata-6.85.1/src/shareddata.egg-info}/PKG-INFO +1 -1
- {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/SOURCES.txt +1 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/LICENSE +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/MANIFEST.in +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/README.md +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/pyproject.toml +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/setup.cfg +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/apidocs-dark.css +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/auth.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/download_paths.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/download_slots.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/install/worker.ps1 +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/install/worker.sh +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/cache.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/collections.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/data.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/downloads.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/operations.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/schedules.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/sqlite.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/tables.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/timeseries.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/web_routes.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/bootstrap.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/migrate.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/store.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/workers.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/catalog.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/config.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/errors.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/indexes.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/readers.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/service.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Database.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Defaults.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ClientAPI.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/LogReader.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/MongoDBClient.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/SaveTables.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Listing.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/LockWaiters.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Logger.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MCP/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MCP/__main__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MCP/client.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MCP/server.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ContainerHealth.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/JobStore.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/RoutineCommand.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/RoutineOrigin.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/Schedule.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ScheduleMirror.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ScheduleStore.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/Worker.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/WorkerLib.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/WorkerPool.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/SQLite.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/SharedData.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/SharedNumpy.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableDisk.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndex.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitFunctions.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitGenerate.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableProtocol.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableRead.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableRewrite.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Users.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Utils.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/activity_logger.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/app.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/google_logo.png +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/kafka_keys.js +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/logs.css +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/sched.css +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/styles.css +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/auth.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/backends.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/containers.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/identity.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/kafka_view.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/mongo_view.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/redis_view.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/route_health.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/route_registry.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/sched.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/schedule_editor.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/server.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/services.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/shell.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/account.js +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/custom.css +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/favicon.ico +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/favicon.png +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/logo.png +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/logo_96x96.png +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/shell.css +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/shell.js +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/suggest.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/account.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/base.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/claim.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/claim_done.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/dash.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/index.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/login.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/login_2fa.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/partials/navbar.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/partials/scripts.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/recovery_codes.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/shell.html +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/twofactor.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/utils.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/workerpool.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/__init__.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/build_info.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/fleet.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/preflight.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/private_files.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/release_backup.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/server.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/services.py +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/entry_points.txt +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/top_level.txt +0 -0
|
@@ -164,16 +164,24 @@ class SymbolRegistry:
|
|
|
164
164
|
|
|
165
165
|
def identify(self, query, remote=True):
|
|
166
166
|
"""Identity of *query*: a registry symbol, a ticker or vendor alias of exactly one
|
|
167
|
-
registry symbol, or a provider symbol of the registry collection (*remote*).
|
|
167
|
+
registry symbol, or a provider symbol of the registry collection (*remote*).
|
|
168
|
+
Active alias matches take precedence across fields; field order breaks ties.
|
|
169
|
+
Exact registry symbols and aliases with no active match retain their behavior."""
|
|
168
170
|
query = str(query or '').strip()
|
|
169
171
|
if not query:
|
|
170
172
|
raise BadRequest('a symbol is required')
|
|
171
173
|
self._load()
|
|
172
174
|
if query in self._rows:
|
|
173
175
|
return self._known(query, query, self._facts(self._rows[query]), 'metadata')
|
|
176
|
+
matches = [(name, lookup.get(query, [])) for name, lookup in self._reverse.items()]
|
|
177
|
+
if self._records is not None and 'isactive' in self._records.dtype.names:
|
|
178
|
+
active = {position for _, positions in matches for position in positions
|
|
179
|
+
if self._records['isactive'][position] == 1}
|
|
180
|
+
if active:
|
|
181
|
+
matches = [(name, [position for position in positions if position in active])
|
|
182
|
+
for name, positions in matches]
|
|
174
183
|
candidates = []
|
|
175
|
-
for name,
|
|
176
|
-
positions = lookup.get(query, [])
|
|
184
|
+
for name, positions in matches:
|
|
177
185
|
if len(positions) == 1:
|
|
178
186
|
return self._known(query, self._symbol(positions[0]), self._facts(positions[0]), f'alias:{name}')
|
|
179
187
|
candidates.extend(self._symbol(position) for position in positions)
|
|
@@ -912,9 +912,38 @@ class RedactingStream:
|
|
|
912
912
|
return getattr(self.stream, name)
|
|
913
913
|
|
|
914
914
|
|
|
915
|
+
class _ChildDebugFilter(logging.Filter):
|
|
916
|
+
"""Keep Flask's level-less default handler from printing propagated DEBUG records."""
|
|
917
|
+
def __init__(self):
|
|
918
|
+
super().__init__()
|
|
919
|
+
self.names = set()
|
|
920
|
+
|
|
921
|
+
def filter(self, record):
|
|
922
|
+
return record.levelno > logging.DEBUG or record.name in self.names
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _quiet_flask_default_handler(logger):
|
|
926
|
+
"""Reading ``app.logger`` makes Flask attach its default handler, which has no
|
|
927
|
+
level, to a logger named after the app (``SharedData.API``). Every child logger
|
|
928
|
+
under that dotted name (the server's own, set to DEBUG with an INFO console
|
|
929
|
+
handler) propagates into it, so each ``#heartbeat#`` debug record was printed
|
|
930
|
+
again in Flask's format. Only the app logger's own DEBUG records stay visible;
|
|
931
|
+
INFO and above from children are untouched.
|
|
932
|
+
"""
|
|
933
|
+
from flask.logging import default_handler
|
|
934
|
+
if default_handler not in logger.handlers:
|
|
935
|
+
return
|
|
936
|
+
quiet = getattr(default_handler, '_shareddata_child_debug', None)
|
|
937
|
+
if quiet is None:
|
|
938
|
+
quiet = default_handler._shareddata_child_debug = _ChildDebugFilter()
|
|
939
|
+
default_handler.addFilter(quiet)
|
|
940
|
+
quiet.names.add(logger.name)
|
|
941
|
+
|
|
942
|
+
|
|
915
943
|
def install_flask_redaction(app):
|
|
916
944
|
"""Protect Flask logging and its request-local wsgi.errors writer."""
|
|
917
945
|
install_redaction(app.logger)
|
|
946
|
+
_quiet_flask_default_handler(app.logger)
|
|
918
947
|
if not getattr(app, '_shareddata_redaction', False):
|
|
919
948
|
original = app.wsgi_app
|
|
920
949
|
def redacted_wsgi(environ, start_response):
|
|
@@ -17,6 +17,8 @@ import asyncio
|
|
|
17
17
|
import importlib
|
|
18
18
|
import bson
|
|
19
19
|
import mmap
|
|
20
|
+
import stat
|
|
21
|
+
import uuid
|
|
20
22
|
|
|
21
23
|
from SharedData.Logger import Logger
|
|
22
24
|
from SharedData.LockWaiters import TableBusy
|
|
@@ -635,14 +637,18 @@ class Table:
|
|
|
635
637
|
Raises:
|
|
636
638
|
Exception: If the table does not exist and cannot be created, or if the index cannot be properly created.
|
|
637
639
|
"""
|
|
638
|
-
|
|
640
|
+
replace_image = self.type == 1 and self.filepath.exists()
|
|
641
|
+
if replace_image and not self.overwrite:
|
|
639
642
|
from SharedData.TableRewrite import RewriteError
|
|
640
643
|
raise RewriteError('overwriting an existing disk image requires staged publication')
|
|
641
644
|
if (not self.names is None) \
|
|
642
645
|
& (not self.formats is None)\
|
|
643
646
|
& (not self.size is None):
|
|
644
|
-
self.create_header()
|
|
645
|
-
|
|
647
|
+
self.create_header()
|
|
648
|
+
if replace_image:
|
|
649
|
+
self._publish_empty_image()
|
|
650
|
+
else:
|
|
651
|
+
self.malloc(create=True)
|
|
646
652
|
|
|
647
653
|
elif (not self.exists) | (self.overwrite):
|
|
648
654
|
if not self.exists:
|
|
@@ -656,6 +662,52 @@ class Table:
|
|
|
656
662
|
if self.is_schemaless:
|
|
657
663
|
self.init_bson_mmap()
|
|
658
664
|
|
|
665
|
+
def _publish_empty_image(self):
|
|
666
|
+
"""
|
|
667
|
+
Replace the existing disk image with a new empty one (overwrite=True).
|
|
668
|
+
|
|
669
|
+
The new image is built complete under a stage name beside data.bin and
|
|
670
|
+
installed by one os.replace, inside the caller's STRUCT section, so a
|
|
671
|
+
failure or crash before the replace leaves the old image untouched.
|
|
672
|
+
The mappings made on the stage stay valid after the rename (same inode);
|
|
673
|
+
other processes reopen by path when the epoch is published on exit from
|
|
674
|
+
the structure section. The index and BSON files only describe the old
|
|
675
|
+
records, so they are removed and rebuilt from the new empty header.
|
|
676
|
+
"""
|
|
677
|
+
from SharedData.TableRewrite import (RewriteError, fsync_directory,
|
|
678
|
+
require_rewrite_support)
|
|
679
|
+
require_rewrite_support()
|
|
680
|
+
final = self.filepath
|
|
681
|
+
old = os.stat(final)
|
|
682
|
+
# The writer lock is held: any earlier stage is from a build that died.
|
|
683
|
+
for leftover in final.parent.glob('data.bin.overwrite.*'):
|
|
684
|
+
leftover.unlink()
|
|
685
|
+
stage = final.parent / ('data.bin.overwrite.' + uuid.uuid4().hex)
|
|
686
|
+
self.filepath = stage
|
|
687
|
+
try:
|
|
688
|
+
self.malloc(create=True)
|
|
689
|
+
os.chmod(stage, stat.S_IMODE(old.st_mode))
|
|
690
|
+
try:
|
|
691
|
+
os.chown(stage, old.st_uid, old.st_gid)
|
|
692
|
+
except OSError:
|
|
693
|
+
pass
|
|
694
|
+
os.replace(stage, final)
|
|
695
|
+
except BaseException:
|
|
696
|
+
self.filepath = final
|
|
697
|
+
try:
|
|
698
|
+
stage.unlink()
|
|
699
|
+
except FileNotFoundError:
|
|
700
|
+
pass
|
|
701
|
+
raise
|
|
702
|
+
self.filepath = final
|
|
703
|
+
fsync_directory(final.parent)
|
|
704
|
+
for name in ('bson.bin', 'dateidx.bin', 'pkey.bin', 'symbolidx.bin', 'portidx.bin'):
|
|
705
|
+
try:
|
|
706
|
+
(final.parent / name).unlink()
|
|
707
|
+
except FileNotFoundError:
|
|
708
|
+
pass
|
|
709
|
+
fsync_directory(final.parent)
|
|
710
|
+
|
|
659
711
|
def create_header(self):
|
|
660
712
|
|
|
661
713
|
"""
|
|
@@ -216,6 +216,7 @@ STANDALONE_COMMANDS = {
|
|
|
216
216
|
'ROUTES': {'route': '/admin/routes', 'description': 'Other web services opened from this site', 'type': 'command', 'group': 'Admin', 'master': True},
|
|
217
217
|
'WORKERS': {'route': '/admin/workers', 'description': 'Add workers, join and command tokens, Git credentials', 'type': 'command', 'group': 'Admin', 'master': True},
|
|
218
218
|
'SCHED': {'route': '/admin/sched', 'description': 'Schedules: status of every routine, restart and stop', 'type': 'command', 'group': 'System', 'master': True},
|
|
219
|
+
'PROC': {'route': '/admin/proc', 'description': 'Processes: what each routine running on this server uses, restart and stop', 'type': 'command', 'group': 'System', 'master': True},
|
|
219
220
|
'LOGS': {'route': '/admin/logs', 'description': 'Live tail of the logs', 'type': 'command', 'group': 'System', 'master': True},
|
|
220
221
|
'WORKERPOOL': {'route': '/admin/workerpool', 'description': 'Worker pool: a terminal to the workers, and the repositories they can run', 'type': 'command', 'group': 'System', 'master': True},
|
|
221
222
|
# The services SharedData can use: listed whether or not one is configured, since each page is also where its connection is set.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""The PROC page's data: the routines whose processes run on this server, and the two commands
|
|
2
|
+
a master may send from it (restart, stop).
|
|
3
|
+
|
|
4
|
+
A routine is one line however many processes it has (a routine with several workers is one
|
|
5
|
+
card). Its numbers are those of all its processes together. Platform processes (the Worker, the
|
|
6
|
+
Scheduler, this server) are shown but are not restarted or stopped from here: their Worker keeps
|
|
7
|
+
them running, and stopping the server would stop the page. So are processes that are not a script
|
|
8
|
+
the Worker runs (an installed program such as vllm or rtxclaw): a restart would try to start that
|
|
9
|
+
file as a Python script.
|
|
10
|
+
"""
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
|
|
17
|
+
import psutil
|
|
18
|
+
|
|
19
|
+
from SharedData.Routines.RoutineCommand import is_scheduler
|
|
20
|
+
from SharedData.Web import sched
|
|
21
|
+
|
|
22
|
+
PIDS_SHOWN = 20 # process ids listed on a card
|
|
23
|
+
SAMPLE_FRESH = 30 # seconds a CPU reading can serve as the start of the next one
|
|
24
|
+
SAMPLE_WAIT = 0.25 # seconds the first reading of a process is taken over
|
|
25
|
+
BAD_STATES = ('zombie', 'dead', 'stopped', 'stop')
|
|
26
|
+
# What the page asks for -> the worker's job (its own 'stop' does nothing).
|
|
27
|
+
JOBS = {job: sched.JOBS[job] for job in ('restart', 'stop')}
|
|
28
|
+
|
|
29
|
+
_samples = {} # (pid, start) -> (monotonic seconds, cpu seconds), kept between two readings
|
|
30
|
+
_lock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _script(command):
|
|
34
|
+
"""What follows 'user@COMPUTER:' in a routine's name, as the SCHED page writes it."""
|
|
35
|
+
repo, routine = command.get('repo') or '', command.get('routine') or ''
|
|
36
|
+
if repo == 'SharedData':
|
|
37
|
+
return 'SharedData.' + routine
|
|
38
|
+
branch = command.get('branch')
|
|
39
|
+
return repo + ('#' + branch if branch and branch != 'nan' else '') + '/' + (routine[:-3] if routine.endswith('.py') else routine)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _key(command):
|
|
43
|
+
parts = [str(command.get(name) or '') for name in ('repo', 'branch', 'routine', 'args')]
|
|
44
|
+
return hashlib.sha1('\0'.join(parts).encode()).hexdigest()[:12]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cpu_seconds(process):
|
|
48
|
+
times = process.cpu_times()
|
|
49
|
+
return times.user + times.system
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read_cpu(processes):
|
|
53
|
+
"""{(pid, start): percent of one core since the previous reading of that process}. A process
|
|
54
|
+
without a recent reading is measured over one short wait, shared by all of them; the page
|
|
55
|
+
asks every few seconds, so the later readings do not wait."""
|
|
56
|
+
first = time.monotonic()
|
|
57
|
+
by_key, spent = {}, {}
|
|
58
|
+
for process in processes:
|
|
59
|
+
try:
|
|
60
|
+
key = (process.pid, process.create_time())
|
|
61
|
+
by_key[key], spent[key] = process, _cpu_seconds(process)
|
|
62
|
+
except psutil.Error:
|
|
63
|
+
continue
|
|
64
|
+
with _lock:
|
|
65
|
+
known = {key: _samples[key] for key in by_key if key in _samples and first - _samples[key][0] <= SAMPLE_FRESH}
|
|
66
|
+
percent = {key: max(0.0, (spent[key] - used) / max(first - when, 1e-3) * 100) for key, (when, used) in known.items()}
|
|
67
|
+
new = [key for key in by_key if key not in known]
|
|
68
|
+
if new:
|
|
69
|
+
time.sleep(SAMPLE_WAIT)
|
|
70
|
+
later = time.monotonic()
|
|
71
|
+
for key in new:
|
|
72
|
+
try:
|
|
73
|
+
percent[key] = max(0.0, (_cpu_seconds(by_key[key]) - spent[key]) / max(later - first, 1e-3) * 100)
|
|
74
|
+
except psutil.Error:
|
|
75
|
+
percent[key] = 0.0
|
|
76
|
+
with _lock:
|
|
77
|
+
for key in [key for key in _samples if key not in by_key]:
|
|
78
|
+
del _samples[key]
|
|
79
|
+
for key in by_key:
|
|
80
|
+
_samples[key] = (first, spent[key])
|
|
81
|
+
return percent
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _iso(epoch):
|
|
85
|
+
return datetime.fromtimestamp(epoch, timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _routines():
|
|
89
|
+
from SharedData.Routines.WorkerLib import update_routines
|
|
90
|
+
found = []
|
|
91
|
+
update_routines(found)
|
|
92
|
+
return found
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def rows():
|
|
96
|
+
"""The routines running here, one dict each. '_args' and '_branch' are the real values and
|
|
97
|
+
never leave this module (an argument can be a secret)."""
|
|
98
|
+
found = _routines()
|
|
99
|
+
processes = [routine['process'] for routine in found if routine.get('process') is not None]
|
|
100
|
+
cpu = _read_cpu(processes)
|
|
101
|
+
grouped = {}
|
|
102
|
+
for routine in found:
|
|
103
|
+
process, command = routine.get('process'), routine.get('command') or {}
|
|
104
|
+
try:
|
|
105
|
+
with process.oneshot():
|
|
106
|
+
start = process.create_time()
|
|
107
|
+
item = dict(pid=routine['pid'], cpu=cpu.get((process.pid, start), 0.0), memory=process.memory_info().rss,
|
|
108
|
+
threads=process.num_threads(), state=process.status(), started=start)
|
|
109
|
+
try:
|
|
110
|
+
item['files'] = process.num_fds() if hasattr(process, 'num_fds') else len(process.open_files())
|
|
111
|
+
except psutil.Error:
|
|
112
|
+
item['files'] = None
|
|
113
|
+
except Exception: # the process ended while it was being read
|
|
114
|
+
continue
|
|
115
|
+
key = _key(command)
|
|
116
|
+
row = grouped.setdefault(key, dict(
|
|
117
|
+
key=key, script=_script(command), repo=command.get('repo') or '', branch=command.get('branch') or '',
|
|
118
|
+
routine=command.get('routine') or '', args=sched.mask_args(command.get('args')), _args=command.get('args') or '',
|
|
119
|
+
processes=0, pids=[], cpu=0.0, memory_mb=0.0, threads=0, files=0, states=set(), started=None,
|
|
120
|
+
platform=command.get('repo') == 'SharedData', script_run=str(command.get('routine') or '').endswith('.py')))
|
|
121
|
+
row['processes'] += 1
|
|
122
|
+
row['pids'].append(item['pid'])
|
|
123
|
+
row['cpu'] += item['cpu']
|
|
124
|
+
row['memory_mb'] += item['memory'] / (1 << 20)
|
|
125
|
+
row['threads'] += item['threads']
|
|
126
|
+
row['files'] = None if row['files'] is None or item['files'] is None else row['files'] + item['files']
|
|
127
|
+
row['states'].add(item['state'])
|
|
128
|
+
row['started'] = item['started'] if row['started'] is None else min(row['started'], item['started'])
|
|
129
|
+
out = []
|
|
130
|
+
for row in grouped.values():
|
|
131
|
+
states = sorted(row.pop('states'))
|
|
132
|
+
row['states'] = states
|
|
133
|
+
bad = [state for state in states if state in BAD_STATES]
|
|
134
|
+
script_run = row.pop('script_run')
|
|
135
|
+
row['kind'] = 'platform' if row['platform'] else 'routine' if script_run else 'external'
|
|
136
|
+
row.update(state=(bad or states or ['unknown'])[0], level='error' if bad else 'running',
|
|
137
|
+
cpu=round(row['cpu'], 1), memory_mb=int(row['memory_mb']), pids=sorted(row['pids'])[:PIDS_SHOWN],
|
|
138
|
+
started=_iso(row['started']), commandable=row['kind'] == 'routine')
|
|
139
|
+
out.append(row)
|
|
140
|
+
return sorted(out, key=lambda row: (row['script'].lower(), row['key']))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def view():
|
|
144
|
+
"""Everything the page shows."""
|
|
145
|
+
from SharedData.Web import workerpool
|
|
146
|
+
found = rows()
|
|
147
|
+
shown = [{key: value for key, value in row.items() if not key.startswith('_')} for row in found]
|
|
148
|
+
totals = dict(routines=len(shown), processes=sum(row['processes'] for row in shown), cpu=round(sum(row['cpu'] for row in shown), 1),
|
|
149
|
+
memory_mb=sum(row['memory_mb'] for row in shown), failing=sum(row['level'] == 'error' for row in shown))
|
|
150
|
+
return dict(worker=workerpool.local_worker().upper(), read_at=_iso(time.time()), viewer=sched.viewer(), rows=shown, totals=totals)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def command(key, job):
|
|
154
|
+
"""The worker command for *job* on the routine whose card has this key. LookupError when it
|
|
155
|
+
no longer runs here, PermissionError for a platform process, ValueError for a wrong job."""
|
|
156
|
+
from SharedData.Web import workerpool
|
|
157
|
+
if not isinstance(job, str) or job not in JOBS:
|
|
158
|
+
raise ValueError('job must be one of: ' + ', '.join(JOBS))
|
|
159
|
+
found = [row for row in rows() if row['key'] == key] if isinstance(key, str) else []
|
|
160
|
+
if not found:
|
|
161
|
+
raise LookupError('that routine no longer runs on this server')
|
|
162
|
+
row = found[0]
|
|
163
|
+
if row['kind'] != 'routine' or is_scheduler(row['script']):
|
|
164
|
+
raise PermissionError('this process is not a routine of the Worker (a platform process, or an installed program): it is not restarted or stopped from here')
|
|
165
|
+
command = {'sender': 'MASTER', 'job': JOBS[job], 'target': workerpool.local_worker(), 'repo': row['repo'], 'routine': row['routine']}
|
|
166
|
+
if row['branch'] and row['branch'] != 'nan':
|
|
167
|
+
command['branch'] = row['branch']
|
|
168
|
+
if row['_args'] and str(row['_args']) != 'nan':
|
|
169
|
+
command['args'] = str(row['_args'])
|
|
170
|
+
return command
|
|
@@ -31,7 +31,7 @@ def csrf_valid():
|
|
|
31
31
|
return bool(provided and expected and hmac.compare_digest(provided.encode(), expected.encode()))
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
ADMIN_PAGES = {'users': 'Users', 'data': 'Data', 'routes': 'Routes', 'workers': 'Workers', 'sched': 'Schedules', 'logs': 'Logs',
|
|
34
|
+
ADMIN_PAGES = {'users': 'Users', 'data': 'Data', 'routes': 'Routes', 'workers': 'Workers', 'sched': 'Schedules', 'proc': 'Processes', 'logs': 'Logs',
|
|
35
35
|
'workerpool': 'Worker pool', 'redis': 'Redis', 'kafka': 'Kafka', 'mongo': 'MongoDB'}
|
|
36
36
|
|
|
37
37
|
|
|
@@ -575,6 +575,36 @@ def init_standalone_routes(app, shdata):
|
|
|
575
575
|
Logger.log.info('SCHED: %s of %s sent by %s' % (data.get('job'), data.get('routine'), g.get('current_identity')))
|
|
576
576
|
return jsonify(sent={key: value for key, value in command.items() if key != 'args'}) # args may hold a secret
|
|
577
577
|
|
|
578
|
+
# PROC: the routines whose processes run on this server, and restart/stop of one.
|
|
579
|
+
@bp.get('/api/web/proc')
|
|
580
|
+
def proc_state():
|
|
581
|
+
from SharedData.Web import proc
|
|
582
|
+
denied = route_admin()
|
|
583
|
+
if denied:
|
|
584
|
+
return denied
|
|
585
|
+
return jsonify(proc.view())
|
|
586
|
+
|
|
587
|
+
@bp.post('/api/web/proc/command')
|
|
588
|
+
def proc_command():
|
|
589
|
+
from SharedData.Logger import Logger
|
|
590
|
+
from SharedData.Routines.WorkerPool import WorkerPool
|
|
591
|
+
from SharedData.Web import proc
|
|
592
|
+
denied = route_admin()
|
|
593
|
+
if denied:
|
|
594
|
+
return denied
|
|
595
|
+
data = request.get_json(silent=True) or {}
|
|
596
|
+
try:
|
|
597
|
+
command = proc.command(data.get('key'), data.get('job'))
|
|
598
|
+
except LookupError as exc:
|
|
599
|
+
return jsonify(error=str(exc)), 404
|
|
600
|
+
except PermissionError as exc:
|
|
601
|
+
return jsonify(error=str(exc)), 409
|
|
602
|
+
except ValueError as exc:
|
|
603
|
+
return jsonify(error=str(exc)), 400
|
|
604
|
+
WorkerPool.post_commands(shdata, [dict(command)])
|
|
605
|
+
Logger.log.info('PROC: %s of %s sent by %s' % (data.get('job'), command['repo'] + '/' + command['routine'], g.get('current_identity')))
|
|
606
|
+
return jsonify(sent={key: value for key, value in command.items() if key != 'args'}) # args may hold a secret
|
|
607
|
+
|
|
578
608
|
def _jobs_list(args):
|
|
579
609
|
"""What the Jobs tab's addresses have in common: ?month=YYYYMM ?status=A,B ?q=text."""
|
|
580
610
|
return {'month': args.get('month') or None, 'statuses': [s for s in args.get('status', '').split(',') if s], 'text': args.get('q') or None}
|
|
@@ -385,6 +385,19 @@ body.resizing { cursor: col-resize; user-select: none; }
|
|
|
385
385
|
.sched-fact .hint.inline { flex-basis: 100%; }
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/* PROC: a card for each routine running here, in SCHED's colours */
|
|
389
|
+
.sched-main.proc-main { grid-template-columns: .6rem 5.4rem minmax(10rem, 1fr) 4.4rem 5.2rem 2.6rem 3.6rem; }
|
|
390
|
+
.proc-num { color: var(--ink); font-size: .82rem; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
391
|
+
.proc-cpu[data-hot] { color: #ffd479; font-weight: 600; }
|
|
392
|
+
@media (min-width: 721px) { .proc-row { grid-template-columns: minmax(0, 1fr) 8.2rem; } .proc-row .sched-actions { justify-content: flex-end; } }
|
|
393
|
+
.proc-platform { font-size: .72rem; }
|
|
394
|
+
.proc-procs { color: var(--muted); }
|
|
395
|
+
@media (max-width: 720px) {
|
|
396
|
+
.sched-main.proc-main { grid-template-columns: .6rem auto minmax(0, 1fr) auto; grid-template-areas: "dot status name up" "dot cpu ram procs"; gap: .1rem .45rem; }
|
|
397
|
+
.proc-main .sched-dot { grid-area: dot; } .proc-main .sched-status { grid-area: status; } .proc-main .sched-name { grid-area: name; } .proc-main .proc-up { grid-area: up; }
|
|
398
|
+
.proc-main .proc-cpu { grid-area: cpu; text-align: left; } .proc-main .proc-ram { grid-area: ram; text-align: left; } .proc-main .proc-procs { grid-area: procs; text-align: left; }
|
|
399
|
+
}
|
|
400
|
+
|
|
388
401
|
/* WORKERS: how each worker is doing, from its heartbeat */
|
|
389
402
|
/* WORKERS, Status: a card for each worker, as the Dashboard's workers tab shows them. */
|
|
390
403
|
.worker-hosts { display: grid; gap: .35rem; }
|
|
@@ -1544,6 +1544,138 @@ if (schedList) {
|
|
|
1544
1544
|
if (view.tab === 'jobs') loadJobs();
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
|
+
// PROC: what each routine running on this server uses, read again every 5 seconds; restart and stop of a routine.
|
|
1548
|
+
const procList = document.querySelector('#proc-list');
|
|
1549
|
+
if (procList) {
|
|
1550
|
+
const KEY = 'shareddata.proc', SORTS = ['name', 'cpu', 'memory', 'started'];
|
|
1551
|
+
const saved = (() => { try { return JSON.parse(localStorage.getItem(KEY)) || {}; } catch (_) { return {}; } })();
|
|
1552
|
+
const view = {q: '', failing: saved.failing === true, sort: SORTS.includes(saved.sort) ? saved.sort : 'name', open: new Set(), filters: saved.filters === true};
|
|
1553
|
+
let data = null, timer;
|
|
1554
|
+
const sent = {};
|
|
1555
|
+
const summary = document.querySelector('#proc-summary'), tools = document.querySelector('#proc-tools'), filter = document.querySelector('#proc-q');
|
|
1556
|
+
const pills = document.querySelector('#proc-levels'), sorting = document.querySelector('#proc-sort'), empty = document.querySelector('#proc-empty');
|
|
1557
|
+
const failed = document.querySelector('#proc-error'), filtersBtn = document.querySelector('#proc-filters');
|
|
1558
|
+
const remember = () => localStorage.setItem(KEY, JSON.stringify({failing: view.failing, sort: view.sort, filters: view.filters}));
|
|
1559
|
+
const clock = iso => iso ? new Date(iso).toLocaleTimeString([], {hour12: false}) : '';
|
|
1560
|
+
const memory = mb => mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb + ' MB';
|
|
1561
|
+
const up = row => spanText((Date.now() - Date.parse(row.started)) / 1000);
|
|
1562
|
+
function paintFilters() {
|
|
1563
|
+
tools.hidden = !view.filters;
|
|
1564
|
+
filtersButton(filtersBtn, view.filters, (view.q ? 1 : 0) + (view.failing ? 1 : 0) + (view.sort !== 'name' ? 1 : 0));
|
|
1565
|
+
}
|
|
1566
|
+
function openFilters(open) { view.filters = open; remember(); paintFilters(); }
|
|
1567
|
+
filtersBtn.addEventListener('click', () => { openFilters(!view.filters); if (view.filters) filter.focus(); });
|
|
1568
|
+
function matches(row) {
|
|
1569
|
+
if (view.failing && row.level !== 'error') return false;
|
|
1570
|
+
return !view.q || [row.script, row.repo, row.branch, row.args, row.routine].some(text => String(text).toLowerCase().includes(view.q));
|
|
1571
|
+
}
|
|
1572
|
+
const ORDER = {
|
|
1573
|
+
name: (a, b) => a.script.localeCompare(b.script, undefined, {sensitivity: 'base'}),
|
|
1574
|
+
cpu: (a, b) => b.cpu - a.cpu || ORDER.name(a, b),
|
|
1575
|
+
memory: (a, b) => b.memory_mb - a.memory_mb || ORDER.name(a, b),
|
|
1576
|
+
started: (a, b) => Date.parse(b.started) - Date.parse(a.started) || ORDER.name(a, b)};
|
|
1577
|
+
function name(row) { // 'Repo#branch/folder/' muted, the code's own name strong
|
|
1578
|
+
const cut = row.script.includes('/') ? row.script.lastIndexOf('/') + 1 : 0;
|
|
1579
|
+
return el('span', {class: 'sched-name'}, cut ? el('span', {class: 'sched-path'}, row.script.slice(0, cut)) : null, el('strong', {}, row.script.slice(cut)));
|
|
1580
|
+
}
|
|
1581
|
+
function ask(row, job) {
|
|
1582
|
+
const verb = job === 'restart' ? 'Restart' : 'Stop';
|
|
1583
|
+
const box = el('dialog', {class: 'log-popup sched-confirm'},
|
|
1584
|
+
el('p', {}, el('strong', {}, `${verb} this routine?`)),
|
|
1585
|
+
el('p', {class: 'sched-routine'}, row.script + (row.args ? ' ' + row.args : '')),
|
|
1586
|
+
el('p', {class: 'hint'}, `on ${data.worker}` + (row.processes > 1 ? ` · ${row.processes} processes` : '')),
|
|
1587
|
+
el('p', {class: 'hint'}, job === 'restart' ? 'The worker ends the routine and starts it again with the newest code.'
|
|
1588
|
+
: 'The worker ends the routine. The Scheduler starts a realtime routine again once it has been silent for a while (45 seconds to 5 minutes): to keep it stopped, take it out of the schedule.'),
|
|
1589
|
+
data.viewer ? el('p', {class: 'hint', 'data-viewer': ''}, 'This server is a viewer: the command is recorded here and reaches no worker, so the routine goes on as it is.') : null,
|
|
1590
|
+
el('div', {class: 'actions'},
|
|
1591
|
+
el('button', {type: 'button', autofocus: true, onclick: () => box.close()}, 'Cancel'),
|
|
1592
|
+
el('button', {type: 'button', class: 'danger', 'data-confirm': job, onclick: async event => {
|
|
1593
|
+
event.target.disabled = true;
|
|
1594
|
+
try {
|
|
1595
|
+
await send('POST', '/api/web/proc/command', {key: row.key, job});
|
|
1596
|
+
sent[row.key] = {job, at: Date.now(), started: row.started};
|
|
1597
|
+
result.textContent = `${verb} sent: ${row.script}` + (data.viewer ? ' (on this viewer only: no worker receives it)' : '');
|
|
1598
|
+
} catch (error) { failure(error); }
|
|
1599
|
+
box.close(); paint();
|
|
1600
|
+
}}, verb)));
|
|
1601
|
+
box.addEventListener('click', event => { if (event.target === box) box.close(); });
|
|
1602
|
+
box.addEventListener('close', () => box.remove());
|
|
1603
|
+
document.body.append(box); box.showModal();
|
|
1604
|
+
}
|
|
1605
|
+
function details(row) {
|
|
1606
|
+
const fact = (label, value) => value || value === 0 ? el('div', {class: 'sched-fact'}, el('span', {class: 'hint inline'}, label), el('span', {}, String(value))) : null;
|
|
1607
|
+
const more = row.processes - row.pids.length;
|
|
1608
|
+
return el('div', {class: 'sched-details'},
|
|
1609
|
+
fact('Repository', row.repo + (row.branch ? ' · branch ' + row.branch : '')), fact('Code', row.routine), fact('Arguments', row.args),
|
|
1610
|
+
fact(row.processes > 1 ? 'Processes' : 'Process', row.pids.join(', ') + (more > 0 ? ` and ${more} more` : '')),
|
|
1611
|
+
fact('Threads', row.threads), fact('Open files', row.files), fact('State', row.states.join(', ')), fact('Started', clock(row.started)),
|
|
1612
|
+
row.commandable ? null : el('p', {class: 'hint'}, row.kind === 'platform' ? 'A platform process: its Worker keeps it running, so it is not restarted or stopped from here.'
|
|
1613
|
+
: 'An installed program, not a script the Worker runs: it is listed for what it uses and is not restarted or stopped from here.'),
|
|
1614
|
+
el('div', {class: 'actions'}, el('button', {type: 'button', onclick: async event => {
|
|
1615
|
+
try { await navigator.clipboard.writeText(row.script); event.target.textContent = 'Copied'; } catch (_) { event.target.textContent = row.script; }
|
|
1616
|
+
}}, 'Copy routine name')));
|
|
1617
|
+
}
|
|
1618
|
+
function line(row) {
|
|
1619
|
+
const open = view.open.has(row.key), mark = sent[row.key];
|
|
1620
|
+
const main = el('div', {class: 'sched-main proc-main', role: 'button', tabindex: '0', 'aria-expanded': String(open),
|
|
1621
|
+
onclick: () => { open ? view.open.delete(row.key) : view.open.add(row.key); paint(); },
|
|
1622
|
+
onkeydown: event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.currentTarget.click(); } }},
|
|
1623
|
+
el('span', {class: 'sched-dot', 'aria-hidden': 'true'}),
|
|
1624
|
+
el('span', {class: 'sched-status'}, row.level === 'error' ? row.state.toUpperCase() : 'RUNNING'),
|
|
1625
|
+
name(row),
|
|
1626
|
+
el('span', {class: 'proc-num proc-cpu', title: 'CPU: percent of one core', 'data-hot': row.cpu >= 80 ? '' : null}, row.cpu.toFixed(1) + '%'),
|
|
1627
|
+
el('span', {class: 'proc-num proc-ram', title: 'Memory in use'}, memory(row.memory_mb)),
|
|
1628
|
+
el('span', {class: 'proc-num proc-procs', title: 'Processes'}, row.processes > 1 ? '×' + row.processes : ''),
|
|
1629
|
+
el('span', {class: 'sched-age proc-up', 'data-started': row.started, title: 'Running for'}, up(row)));
|
|
1630
|
+
return el('div', {class: 'sched-row proc-row', 'data-level': row.level, 'data-key': row.key}, main,
|
|
1631
|
+
el('span', {class: 'sched-actions'}, mark ? el('span', {class: 'sched-sent'}, `${mark.job} sent ${clock(new Date(mark.at).toISOString())}`) : null,
|
|
1632
|
+
...(row.commandable ? [el('button', {type: 'button', 'data-job': 'restart', onclick: () => ask(row, 'restart')}, 'Restart'),
|
|
1633
|
+
el('button', {type: 'button', 'data-job': 'stop', onclick: () => ask(row, 'stop')}, 'Stop')] : [el('span', {class: 'hint proc-platform'}, row.kind)])),
|
|
1634
|
+
open ? details(row) : null);
|
|
1635
|
+
}
|
|
1636
|
+
function paint() {
|
|
1637
|
+
if (!data) return;
|
|
1638
|
+
const rows = data.rows, totals = data.totals, bad = rows.filter(row => row.level === 'error').length;
|
|
1639
|
+
summary.textContent = `${data.worker} · ${totals.routines} ${totals.routines === 1 ? 'routine' : 'routines'} · ${totals.processes} ${totals.processes === 1 ? 'process' : 'processes'} · CPU ${totals.cpu.toFixed(0)}% · ${memory(totals.memory_mb)}` + (bad ? ` · ${bad} failing` : '');
|
|
1640
|
+
pills.replaceChildren(...(bad || view.failing ? [el('button', {type: 'button', class: 'pill sched-level', 'data-level': 'error', 'aria-pressed': String(view.failing),
|
|
1641
|
+
onclick: () => { view.failing = !view.failing; remember(); paint(); }}, `Failing ${bad}`)] : []));
|
|
1642
|
+
paintFilters();
|
|
1643
|
+
sorting.value = view.sort;
|
|
1644
|
+
const shown = rows.filter(matches).sort(ORDER[view.sort]);
|
|
1645
|
+
procList.replaceChildren(...shown.map(line));
|
|
1646
|
+
empty.hidden = shown.length > 0;
|
|
1647
|
+
empty.textContent = !rows.length ? 'No routine is running on this server.' : 'No routine matches the filter.';
|
|
1648
|
+
}
|
|
1649
|
+
function times() { // once a second: uptimes go on counting between two readings
|
|
1650
|
+
for (const node of procList.querySelectorAll('.proc-up[data-started]')) node.textContent = spanText((Date.now() - Date.parse(node.dataset.started)) / 1000);
|
|
1651
|
+
}
|
|
1652
|
+
async function load() {
|
|
1653
|
+
clearTimeout(timer);
|
|
1654
|
+
try {
|
|
1655
|
+
const next = await call('/api/web/proc');
|
|
1656
|
+
data = next; failed.hidden = true;
|
|
1657
|
+
for (const [key, mark] of Object.entries(sent)) { // a mark stays until the routine has started again, or a minute
|
|
1658
|
+
const row = data.rows.find(candidate => candidate.key === key);
|
|
1659
|
+
if (!row || row.started !== mark.started || Date.now() - mark.at > 60000) delete sent[key];
|
|
1660
|
+
}
|
|
1661
|
+
if (!String(getSelection()) || !procList.contains(getSelection().anchorNode)) paint();
|
|
1662
|
+
} catch (error) { failed.hidden = false; failed.textContent = 'Cannot read the processes: ' + error.message; }
|
|
1663
|
+
if (!document.hidden) timer = setTimeout(load, 5000);
|
|
1664
|
+
}
|
|
1665
|
+
filter.addEventListener('input', () => { view.q = filter.value.trim().toLowerCase(); paint(); });
|
|
1666
|
+
sorting.addEventListener('change', () => { view.sort = sorting.value; remember(); paint(); });
|
|
1667
|
+
document.addEventListener('keydown', event => {
|
|
1668
|
+
if (event.ctrlKey || event.metaKey || event.altKey || document.querySelector('dialog[open]')) return;
|
|
1669
|
+
if (/^(INPUT|TEXTAREA|SELECT)$/.test(event.target.tagName)) { if (event.key === 'Escape') event.target.blur(); return; }
|
|
1670
|
+
if (event.key === '/') { event.preventDefault(); openFilters(true); filter.focus(); }
|
|
1671
|
+
else if (event.key.toLowerCase() === 'f') pills.querySelector('[data-level=error]')?.click();
|
|
1672
|
+
});
|
|
1673
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) load(); });
|
|
1674
|
+
setInterval(times, 1000);
|
|
1675
|
+
paintFilters();
|
|
1676
|
+
load();
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1547
1679
|
// Shared by WORKERPOOL and REDIS: a node's children without the ones that are not there, and
|
|
1548
1680
|
// sizes and spans as a person reads them.
|
|
1549
1681
|
function fill(node, ...children) { node.replaceChildren(...children.filter(child => child != null)); }
|
|
@@ -94,6 +94,13 @@
|
|
|
94
94
|
<p><button id="jobs-more" type="button" hidden>Show more</button></p>
|
|
95
95
|
</div>
|
|
96
96
|
</section>{% endif %}
|
|
97
|
+
{% if section == 'proc' %}<section id="page-proc" class="page tight sched proc">
|
|
98
|
+
<div class="head"><h2>Processes</h2><button id="proc-filters" class="filters-toggle" type="button" aria-expanded="false">Filters</button><span id="proc-summary" class="hint" role="status"></span></div>
|
|
99
|
+
<div id="proc-tools" class="toolbar sched-toolbar" hidden><input id="proc-q" type="search" placeholder="Filter: routine, repository, branch or arguments" aria-label="Filter routines"><span id="proc-levels" class="pills" role="group" aria-label="States shown"></span><select id="proc-sort" aria-label="Sort by"><option value="name">By name</option><option value="cpu">By CPU</option><option value="memory">By memory</option><option value="started">Newest first</option></select></div>
|
|
100
|
+
<p id="proc-error" class="sched-stale" role="alert" hidden></p>
|
|
101
|
+
<div id="proc-list" class="sched-list" aria-live="off"></div>
|
|
102
|
+
<p id="proc-empty" class="hint" hidden></p>
|
|
103
|
+
</section>{% endif %}
|
|
97
104
|
{% if section == 'workerpool' %}<section id="page-workerpool" class="page tight sched workerpool">
|
|
98
105
|
<div class="head"><h2>Worker pool</h2><span id="wp-count" class="hint" role="status"></span><button id="wp-repos-btn" type="button" aria-expanded="false" aria-controls="wp-repos">Repositories</button><button id="wp-procs-btn" type="button" aria-expanded="false" aria-controls="wp-procs">Processes</button></div>
|
|
99
106
|
<div id="wp-workers" class="wp-workers" role="listbox" aria-label="Where the commands go"></div>
|
|
@@ -132,6 +132,7 @@ src/SharedData/Web/containers.py
|
|
|
132
132
|
src/SharedData/Web/identity.py
|
|
133
133
|
src/SharedData/Web/kafka_view.py
|
|
134
134
|
src/SharedData/Web/mongo_view.py
|
|
135
|
+
src/SharedData/Web/proc.py
|
|
135
136
|
src/SharedData/Web/redis_view.py
|
|
136
137
|
src/SharedData/Web/route_health.py
|
|
137
138
|
src/SharedData/Web/route_registry.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|