1+ import json
12import logging
23from collections .abc import Mapping
34from dataclasses import dataclass
89from bluesky .callbacks .tiled_writer import TiledWriter
910from bluesky_stomp .messaging import StompClient
1011from bluesky_stomp .models import Broker , DestinationBase , MessageTopic
12+ from fastapi import status
1113from tiled .client import from_uri
14+ from tiled .client .utils import ClientError
1215
1316from blueapi .cli .scratch import get_python_environment
1417from blueapi .config import ApplicationConfig , OIDCConfig , ServiceAccount , StompConfig
@@ -103,13 +106,60 @@ def setup(config: ApplicationConfig) -> None:
103106 stomp_client ()
104107
105108
109+ @cache
110+ def service_account_tiled_client ():
111+ """One client for every task: its auth refreshes its own token, so unlike a
112+ client carrying a caller's pass-through headers it can be reused, which saves
113+ the server-info and token requests from_uri makes on every task."""
114+ tiled_config = context ().tiled_conf
115+ assert tiled_config is not None and isinstance (
116+ tiled_config .authentication , ServiceAccount
117+ )
118+ return from_uri (
119+ str (tiled_config .url ), auth = TiledAuth (tiled_auth = tiled_config .authentication )
120+ )
121+
122+
123+ def _tiled_session_container (tiled_client , instrument : str , instrument_session : str ):
124+ """The beamline/proposal/visit container runs are written under.
125+
126+ It almost always exists already, so it is looked up in one request; only the
127+ first task for a visit walks the levels and creates what is missing.
128+ """
129+ session_blob = access_blob (instrument_session , instrument )
130+ proposal = session_blob ["proposal" ]
131+ try :
132+ return tiled_client [f"{ instrument } /{ proposal } /{ instrument_session } " ]
133+ except KeyError :
134+ pass
135+ # Each level's access blob is the prefix of the full (beamline, proposal,
136+ # visit) one, matching the beamline/proposal/session tiers the tiled access
137+ # policy expects a container to be tagged with.
138+ level_access_blobs = {
139+ instrument : {"beamline" : instrument },
140+ proposal : {"beamline" : instrument , "proposal" : proposal },
141+ instrument_session : session_blob ,
142+ }
143+ for key , blob in level_access_blobs .items ():
144+ try :
145+ tiled_client = tiled_client .create_container (
146+ key = key , access_tags = [json .dumps (blob )]
147+ )
148+ except ClientError as e :
149+ if e .response .status_code != status .HTTP_409_CONFLICT :
150+ raise
151+ tiled_client = tiled_client [key ] # created already, e.g. by another task
152+ return tiled_client
153+
154+
106155def teardown () -> None :
107156 worker ().stop ()
108157 if (stomp_client_ref := stomp_client ()) is not None :
109158 stomp_client_ref .disconnect ()
110159 context .cache_clear ()
111160 worker .cache_clear ()
112161 stomp_client .cache_clear ()
162+ service_account_tiled_client .cache_clear ()
113163
114164
115165def _publish_event_streams (
@@ -166,7 +216,7 @@ def submit_task(
166216 # We raise an InvalidConfigError on setting tiled_conf if this isn't set
167217 assert md
168218 metadata ["tiled_access_tags" ] = [
169- access_blob (task_request .instrument_session , md .instrument )
219+ json . dumps ( access_blob (task_request .instrument_session , md .instrument ) )
170220 ]
171221 task = Task (
172222 name = task_request .name ,
@@ -191,21 +241,26 @@ def begin_task(
191241 if nt := active_context .numtracker :
192242 nt .set_headers (pass_through_headers or {})
193243
244+ if task .task_id is None :
245+ return task
246+
194247 subscribers = []
195248 if tiled_config := active_context .tiled_conf :
196249 # Tiled queries the root node, so must create an authorized client
197250 if isinstance (tiled_config .authentication , ServiceAccount ):
198- tiled_client = from_uri (
199- str (tiled_config .url ),
200- auth = TiledAuth (tiled_auth = tiled_config .authentication ),
201- )
251+ tiled_client = service_account_tiled_client ()
202252 else :
203253 tiled_client = from_uri (
204254 str (tiled_config .url ),
205255 api_key = tiled_config .authentication ,
206256 headers = pass_through_headers ,
207257 )
208-
258+ if (task_ := get_task_by_id (task_id = task .task_id )) is not None :
259+ tiled_client = _tiled_session_container (
260+ tiled_client ,
261+ active_context .run_engine .md ["instrument" ],
262+ task_ .task .metadata ["instrument_session" ],
263+ )
209264 tiled_writer_token = active_context .run_engine .subscribe (
210265 TiledWriter (tiled_client , batch_size = 1 )
211266 )
@@ -227,13 +282,12 @@ def remove_callback_when_task_finished(
227282 )
228283 subscribers .append ((active_worker .worker_events , remove_callback ))
229284
230- if task .task_id is not None :
231- try :
232- active_worker .begin_task (task .task_id )
233- except :
234- for channel , token in subscribers :
235- channel .unsubscribe (token )
236- raise
285+ try :
286+ active_worker .begin_task (task .task_id )
287+ except Exception :
288+ for channel , token in subscribers :
289+ channel .unsubscribe (token )
290+ raise
237291 return task
238292
239293
0 commit comments