본문으로 건너뛰기

Runtime v2 Client Wiring

플러그인 런타임이 이제 모든 RuntimeContext 에 기존 v1 BackendClient 와 함께 BackendClientV2 를 함께 주입합니다. 두 클라이언트가 같은 컨텍스트 객체 위에 놓이므로 — ctx.client (v1) 와 ctx.v2_client (v2) — caller 는 SDK 전체를 한꺼번에 cut-over 하지 않고도 호출 단위로 v2 엔드포인트로 점진 마이그레이션할 수 있습니다.

Ticket: SYN-6919 (parent SYN-6893, label 26H1Patch)

이 변경은 synapse-backend 의 v2 export-readiness 작업 (/v2/tasks/bulk-fetch/, /v2/assignments/bulk-fetch/, /v2/ground-truths/bulk-data/, /v2/ground-truth-events/bulk-data/) 의 SDK 측 짝입니다. 해당 bulk 엔드포인트 대응 BackendClientV2 리소스 메서드는 후속 SDK PR (SDK-2 / SDK-3 / SDK-4) 에서 추가되며, ctx.v2_client.* 로 caller 를 옮기는 작업은 SDK-6 / SDK-7 / SDK-8 에서 이뤄집니다.

shim 대신 두 번째 클라이언트 필드를 두는 이유​

BackendClientV2 는 BackendClient 의 drop-in replacement 가 아닙니다. 페이지네이션 형태, 헤더 이름, 메서드 네임스페이스, 응답 모델이 모두 다릅니다 (전체 비교는 BackendClientV2 — Migration from v1 참고). v1 표면 뒤로 v2 클라이언트를 숨기는 shim 은 v2 만의 동작 (cursor 페이지네이션, request_id 캡처, typed Pydantic 모델) 을 잃거나 v1 caller 를 조용히 깨뜨립니다.

대신 SDK 는 additive 경로를 선택했습니다 — 두 클라이언트를 모두 주입하고, 각 caller 가 어느 쪽을 쓸지 직접 선택합니다. SemVer cut-over 가 없고, 외부 plugin 작성자 입장에서 v1 표면은 그대로입니다.

# RuntimeContext (synapse_sdk/plugins/context/__init__.py)
@dataclass
class RuntimeContext:
logger: BaseLogger
env: PluginEnvironment
job_id: str | None = None
client: BackendClient | None = None # v1 — 변경 없음
v2_client: BackendClientV2 | None = None # v2 — 신규 (SYN-6919, additive)
agent_client: AgentClient | None = None
checkpoint: dict[str, Any] | None = None
language: str | None = None

Factory — create_backend_client_v2()​

synapse_sdk/utils/auth.py 의 신규 factory 는 오래된 create_backend_client() 과 동일한 형태입니다. v1 factory 와 동일한 공유 env 들을 우선 사용하고 v2 전용 fallback 을 추가합니다.

from synapse_sdk.utils.auth import create_backend_client_v2

v2_client = create_backend_client_v2()
# BackendClientV2 또는 None (env / config 어디에도 토큰 없음).

환경 변수 우선순위​

Factory 는 단일 env 해상 패스를 사용합니다 — 두 클라이언트가 같은 SSOT 를 참조하므로 synapse login 한 번 (또는 plugin 런타임이 주입하는 env 블록 하나) 으로 둘 다 부트스트랩됩니다.

우선순위변수용도v1 과 공유?
1SYNAPSE_HOSTBackend base URL (/v2 suffix 없음)예
1SYNAPSE_ACCESS_TOKENsyn_* 액세스 토큰예
2SYNAPSE_PLUGIN_RUN_USER_TOKEN플러그인 런타임이 주입하는 DRF 토큰예 (v1 은 authorization_token 으로, v2 는 drf_token 으로 바인딩)
2SYNAPSE_PLUGIN_RUN_TENANT플러그인 런타임이 주입하는 테넌트 코드예
3SYNAPSE_BACKEND_V2_ACCESS_TOKENv2 전용 access 토큰 fallback아니오
3SYNAPSE_BACKEND_V2_DRF_TOKENv2 전용 DRF 토큰 fallback아니오
3SYNAPSE_BACKEND_V2_TENANTv2 전용 tenant fallback아니오

SYNAPSE_ACCESS_TOKEN, SYNAPSE_PLUGIN_RUN_USER_TOKEN, SYNAPSE_BACKEND_V2_ACCESS_TOKEN, SYNAPSE_BACKEND_V2_DRF_TOKEN 중 아무것도 set 되어 있지 않으면 factory 는 None 을 리턴합니다. v1 factory 와 같은 정책이므로 자격증명이 없는 개발 환경도 계속 실행 가능합니다.

tenant_token_prefix=True 를 강제하는 이유​

이미 배포된 모든 plugin-runtime 은 v1 스타일 SYNAPSE-Tenant: Token <code> 헤더를 가정합니다. 런타임 안에서 v2-native raw 값 (SYNAPSE-Tenant: <code>) 으로 전환하면 진행 중인 plugin run 의 tenant routing 을 조용히 깰 수 있습니다. 그래서 factory 는 tenant_token_prefix=True 를 무조건 전달합니다. v2-native raw 값이 필요한 standalone caller 는 BackendClientV2 를 직접 생성하세요. 헤더별 동작은 BackendClientV2 — Authentication 을 참고하세요.

Wiring 위치 — 5 instantiation site​

RuntimeContext 는 정확히 다섯 곳에서 생성됩니다. 다섯 곳 모두 이제 create_backend_client() 옆에서 create_backend_client_v2() 를 호출하고 결과를 v2_client= 로 전달합니다.

Site파일Trigger
Entrypointsynapse_sdk/plugins/entrypoint.py서브프로세스 main() — plugin 런타임 컨테이너가 exec 하는 entrypoint
LocalExecutorsynapse_sdk/plugins/executors/local.pyIn-process synapse plugin run --mode local
Ray Tasksynapse_sdk/plugins/executors/ray/task.pyRay Actor 기반 --mode task
Ray Jobsynapse_sdk/plugins/executors/ray/job.pyRay Jobs API 기반 --mode job
Ray Pipelinesynapse_sdk/plugins/executors/ray/pipeline.pyRay 멀티 액션 파이프라인

Factory 는 RuntimeContext 빌드당 한 번 호출됩니다. Ray 기반 executor 의 경우 호출이 actor / job 프로세스 내부 에서 일어나므로 driver 가 아니라 런타임이 해당 워커에 주입한 env 블록을 사용합니다.

Caller 패턴 — 클래스 기반 액션​

외부 plugin 작성자는 self.v2_client (클래스 기반 액션) 또는 ctx.v2_client (함수 기반 / step 기반 액션) 로 v2 클라이언트에 접근합니다. 표면은 BackendClientV2 와 완전히 동일합니다 — SDK 측 wrapper 가 없습니다.

from synapse_sdk.plugins.action import BaseAction
from synapse_sdk.plugins.enums import PluginCategory


class MyExportAction(BaseAction):
category = PluginCategory.EXPORT

def run(self):
# v1 경로 — 기존 동작과 동일
if self.client:
projects = self.client.list_projects()

# v2 경로 — 자격증명 없는 환경에선 None 가드 필요
if self.v2_client is not None:
page = self.v2_client.tasks.list(project=42, per_page=50)
for task in page.results:
...

# backend log lookup 용 request_id correlation
with self.v2_client.capture_request_ids():
self.v2_client.assignments.list(project=42, list_all=True)
self.logger.info(
'export-listing',
request_id=self.v2_client.last_request_id,
)

None 가드 규칙​

ctx.client 와 ctx.v2_client 는 모두 Optional 타입이며 개발 환경에서 None 일 수 있습니다. Factory 는 env / ~/.synapse/config.json 어디서든 자격증명을 해상할 수 있을 때만 클라이언트를 리턴합니다. 운영 환경에선 런타임이 자격증명을 주입하지만, 자격증명 없는 셸에서 synapse plugin run --mode local 을 호출하면 두 필드가 모두 None 인 컨텍스트가 만들어집니다. 플러그인 코드는 v2 호출을 if ctx.v2_client is not None: (또는 if self.v2_client is not None:) 으로 가드해야 합니다.

공존 정책 — additive, cut-over 없음​

항목v1 (self.client)v2 (self.v2_client)
필드 가용성변경 없음 — SYN-6919 이전과 동일신규, 기본값 None
Wire 되는 시점SYNAPSE_ACCESS_TOKEN, SYNAPSE_PLUGIN_RUN_USER_TOKEN 중 하나라도 set 된 경우SYNAPSE_ACCESS_TOKEN, SYNAPSE_PLUGIN_RUN_USER_TOKEN, SYNAPSE_BACKEND_V2_ACCESS_TOKEN, SYNAPSE_BACKEND_V2_DRF_TOKEN 중 하나라도 set 된 경우
Tenant 헤더SYNAPSE-Tenant: Token <code>SYNAPSE-Tenant: Token <code> (런타임이 v1 호환 prefix 강제, 위 factory 노트 참고)
페이지네이션Offset (page_size=, 응답에 count)Cursor (per_page=, count 없음)
응답 형태Raw dictTyped Pydantic 모델
마이그레이션 속도n/a호출 단위, 고정 (SYN-7415)

SemVer 영향은 minor (additive). v2_client 를 참조하지 않는 외부 plugin 은 행동 변화가 0 입니다.

고정 caller 라우팅 (선택 스위치 없음)​

v2_client 가 wiring 되어 있다고 해서 caller 가 자동으로 v2 로 라우팅되지는 않습니다 — 하지만 라우팅은 런타임에 선택되는 것이 아니라 callsite 마다 고정 되어 있습니다. SYN-7415 가 v1/v2 를 결정하던 선택 스위치 기계장치 (use_v2 / _V2_CATEGORY_POLICY) 를 제거했습니다 (synapse_sdk/plugins/actions/_v2_switch.py 는 이제 _v2_fetch 로 옮겨간 fetch helper 를 위한 backward-compatible re-export shim 일 뿐입니다):

Caller라우팅
export/handlers.pyv2-fixed — 항상 ctx.v2_client 의 v2 2-phase 경로 수행. v2_client 미배선 시 명시적 ClientError (EXPORT_FAILED_V2_NOT_WIRED) 발생, v1 fallback 없음
to_task/steps/fetch_tasks.pyv1-fixed
dataset/action.py _download_splitv1-fixed

SYNAPSE_FORCE_V1_EXPORT kill-switch 는 더 이상 없습니다 — export 는 v2-fixed 이므로 unconfigured 실행 (v2_client 미배선) 은 v1 로 폴백하는 대신 명시적으로 실패합니다. 전체 라우팅 테이블은 caller 라우팅 — v1/v2 는 선택이 아니라 고정 을 참고하세요.

export 위임도 이 클라이언트를 탑니다

임계 초과 export 의 경우 SDK 는 v2_client.plugin_exports.create(...), v2_client.async_jobs.stream_progress(job_id) (SSE), v2_client.async_jobs.retrieve(job_id) 를 통해 처리를 backend async-job 에 위임합니다 — 따라서 v2_client 가 wiring 되어 있어야 위임 게이트가 발동할 수 있습니다. 위임은 SYNAPSE_EXPORT_DISABLE_ASYNC_DELEGATE / export_disable_async_delegate kill-switch 로 제어되며, 이는 v1/v2 선택과 독립적입니다. Export Actions — 서버사이드 위임 을 참고하세요.

마이그레이션 로드맵​

SDK-1 에 들어간 wiring 은 caller 측 마이그레이션이 들어오기 전까지 의도적으로 inert 합니다 — 현재 in-tree action 중 ctx.v2_client 를 참조하는 것은 없습니다. 세 개의 후속 SDK PR 이 기존 v1 호출 사이트를 v2 표면으로 변환합니다.

후속 PR범위
SDK-2 / SDK-3 / SDK-4bulk 엔드포인트용 v2 리소스 메서드 추가 (tasks.bulk_fetch, assignments.bulk_fetch, ground_truths.bulk_data, ground_truth_events.bulk_data).
SDK-6export action target handler 를 self.client.list_tasks → self.v2_client.tasks.bulk_fetch 로 마이그레이션.
SDK-7assignment / review export 경로 마이그레이션.
SDK-8ground-truth dataset export 경로 마이그레이션.

각 후속 PR 은 독립적이며 v1 경로는 그대로 두므로 외부 plugin 은 계속 그대로 동작합니다. in-tree caller 가 모두 v2 로 이동하면 SDK 는 ctx.client 의 deprecation 여부를 재검토할 것입니다 — 그 결정은 SYN-6919 의 범위 밖입니다.

고정 라우팅 갱신

SDK-6/7/8 caller 는 모두 머지되었습니다. SYN-7082 가 v2 선택을 per-category 정책 뒤로 게이팅했고, SYN-7415 가 선택 스위치를 완전히 제거 했습니다 (위 고정 caller 라우팅 참고). 현재 순효과: export handler 는 v2-fixed, to_task fetch 와 dataset download caller 는 v1-fixed 입니다. caller 를 v1/v2 사이에서 옮기는 일은 이제 런타임 스위치나 정책 테이블 편집이 아니라 callsite 의 SDK 코드 변경입니다.

직접 작성한 plugin 테스트하기​

tests/plugins/context/test_v2_client_wiring.py 는 5개 instantiation site 모두에 대한 wiring 체크를 parametrize 합니다. 새 executor 가 RuntimeContext 를 빌드한다면 그 목록에 추가하세요 — v2_client= 인자 누락이 런타임이 아닌 테스트 단계에서 잡힙니다.

외부 plugin 테스트의 경우 factory 를 monkey-patch 하세요.

import synapse_sdk.utils.auth as auth_module
from synapse_sdk.clients.backend_v2 import BackendClientV2


def test_action_uses_v2_client(monkeypatch):
fake = BackendClientV2('https://api.test.synapse.sh', access_token='syn_x')
monkeypatch.setattr(auth_module, 'create_backend_client_v2', lambda: fake)
# ... LocalExecutor 빌드 / entrypoint 호출 / 등.

See also​