docs: redesign T1 client creation with transport selector pattern, remove grpc.ClientConn from QUIC path
This commit is contained in:
@@ -39,7 +39,7 @@
|
|||||||
- `net/http` 인터페이스 호환 → HTTP/3 서버를 표준 핸들러로 운영 가능
|
- `net/http` 인터페이스 호환 → HTTP/3 서버를 표준 핸들러로 운영 가능
|
||||||
- Connection migration 내장: 핸드오버(IP 변경) 시 세션 유지
|
- Connection migration 내장: 핸드오버(IP 변경) 시 세션 유지
|
||||||
- QUIC Datagram 확장(RFC 9221) 지원 → 저지연 V2X 메시지에 활용 가능
|
- QUIC Datagram 확장(RFC 9221) 지원 → 저지연 V2X 메시지에 활용 가능
|
||||||
- gRPC-Go 트랜스포트 레이어 교체 방식으로 통합 (`WithContextDialer` + `quic.DialAddr`)
|
- TransportSelector 기반의 사전 프로브 및 분기 방식으로 통합 (UDP 성공 시 connect-go/HTTP3 RoundTripper, 실패 시 grpc-go/HTTP2 폴백)
|
||||||
|
|
||||||
#### gRPC-Go (`google.golang.org/grpc`)
|
#### gRPC-Go (`google.golang.org/grpc`)
|
||||||
- 공식 Google Go gRPC 구현 — Interceptor Chain, Health Checking, Service Config(retry policy) 내장
|
- 공식 Google Go gRPC 구현 — Interceptor Chain, Health Checking, Service Config(retry policy) 내장
|
||||||
@@ -554,16 +554,19 @@ var defaultDeadlines = map[string]int64{
|
|||||||
|
|
||||||
T1 클라우드와 T2 엣지 게이트웨이 간 무선 링크의 신뢰성을 보장하기 위해, 기본 UDP/QUIC 연결 수립 실패 시 TCP/HTTP/2 경로로 자동 전환하는 전송 폴백 메커니즘을 구현한다. 본 구조는 포트나 전송 프로토콜이 상이하여 단일 핸드셰이크 내 ALPN 강등 협상이 불가하므로, 클라이언트 단의 다이얼러에서 명시적인 연결 실패 감지 후 폴백을 수행한다.
|
T1 클라우드와 T2 엣지 게이트웨이 간 무선 링크의 신뢰성을 보장하기 위해, 기본 UDP/QUIC 연결 수립 실패 시 TCP/HTTP/2 경로로 자동 전환하는 전송 폴백 메커니즘을 구현한다. 본 구조는 포트나 전송 프로토콜이 상이하여 단일 핸드셰이크 내 ALPN 강등 협상이 불가하므로, 클라이언트 단의 다이얼러에서 명시적인 연결 실패 감지 후 폴백을 수행한다.
|
||||||
|
|
||||||
##### 1. 클라이언트 측 전송 어댑터 및 폴백 다이얼러 (`internal/gateway/fallback_dialer.go`)
|
##### 1. 클라이언트 측 전송 어댑터 및 트랜스포트 셀렉터 (`internal/gateway/fallback_dialer.go`)
|
||||||
|
|
||||||
공식 gRPC-Go의 단일 커넥션 다이얼러 방식은 하나의 QUIC 스트림으로 HTTP/2 전체 프레임을 터널링할 경우 QUIC 세션 내부에서 TCP 수준의 HOLB를 그대로 재현하는 설계 결함(naive 구현)을 갖는다. 이를 방지하기 위해 각 RPC 호출을 독립된 QUIC 스트림에 1:1로 매핑하는 구조를 채택하며, 다음의 두 가지 대안을 명세한다:
|
공식 gRPC-Go의 단일 커넥션 다이얼러 방식은 하나의 QUIC 스트림으로 HTTP/2 전체 프레임을 터널링할 경우 QUIC 세션 내부에서 TCP 수준의 HOLB를 그대로 재현하는 설계 결함(naive 구현)을 갖는다. 이를 방지하기 위해 각 RPC 호출을 독립된 QUIC 스트림에 1:1로 매핑하는 구조를 채택하며, 다음의 두 가지 대안을 명세한다:
|
||||||
|
|
||||||
- **(a) 권장 구현 — HTTP/3 의미론 기반 매핑 (connect-go 활용)**:
|
- **(a) 권장 구현 — HTTP/3 의미론 기반 매핑 (connect-go 활용)**:
|
||||||
`quic-go/http3` 패키지의 `http3.RoundTripper`를 기반으로 동작하는 HTTP/3 클라이언트를 활용하여 gRPC와 와이어 프로토콜 수준에서 호환되도록 구성한다. 이 방식은 HTTP/3 명세에 따라 **개별 RPC 호출(Request/Response)이 네트워크 수준의 독립된 QUIC 스트림으로 자동 매핑**되므로 패킷 유실 시에도 다른 RPC 스트림이 정체되지 않는다.
|
`quic-go/http3` 패키지의 `http3.RoundTripper`를 기반으로 동작하는 HTTP/3 클라이언트를 활용하여 gRPC와 와이어 프로토콜 수준에서 호환되도록 구성한다. 이 방식은 HTTP/3 명세에 따라 **개별 RPC 호출(Request/Response)이 네트워크 수준의 독립된 QUIC 스트림으로 자동 매핑**되므로 패킷 유실 시에도 다른 RPC 스트림이 정체되지 않는다. 이 권장 구현의 Go 코드 세부 사양은 부록 [internal/cloud/grpc_quic_client.go](file:///Users/godopu16/PuKi/lab/canary_projects/multi-agent-paper/gRPC_Based_Interface/IMPL_DESIGN.md#L1498)의 `NewT1Client` QUIC 분기 구현(RoundTripper 및 http.Client 연계)을 통해 구체화된다.
|
||||||
- **(b) 대안 구현 — per-RPC 커스텀 ClientTransport**:
|
- **(b) 대안 구현 — per-RPC 커스텀 ClientTransport**:
|
||||||
gRPC-Go의 커스텀 `ClientTransport` 인터페이스를 직접 구현하여, 클라이언트가 RPC를 개시할 때마다 `quic.Connection.OpenStreamSync`를 호출하여 새 QUIC 스트림을 동적으로 개방하고, 해당 스트림에 length-prefixed gRPC 바이트 프레임을 직접 매핑하여 송수신한다. 단, 이 대안은 gRPC-Go 내부의 private transport API 변동에 따른 유지보수 리스크가 있음을 명시한다.
|
gRPC-Go의 커스텀 `ClientTransport` 인터페이스를 직접 구현하여, 클라이언트가 RPC를 개시할 때마다 `quic.Connection.OpenStreamSync`를 호출하여 새 QUIC 스트림을 동적으로 개방하고, 해당 스트림에 length-prefixed gRPC 바이트 프레임을 직접 매핑하여 송수신한다. 단, 이 대안은 gRPC-Go 내부의 private transport API 변동에 따른 유지보수 리스크가 있음을 명시한다.
|
||||||
|
|
||||||
아래는 다이얼 실패 감지 및 `tls.Config` 분리 복제(`Clone`)가 적용된 폴백 다이얼러 구현 명세이다:
|
> [!IMPORTANT]
|
||||||
|
> **구조적 제약 규정**: QUIC 경로 클라이언트 기동 시, 기존의 naive 터널링 회귀를 원천적으로 방지하기 위해 `grpc.DialContext` 및 `WithContextDialer` API의 사용을 구조적으로 금지한다. QUIC 전송은 오직 `http3.RoundTripper`와 와이어 수준에서 호환되는 HTTP/3 기반 클라이언트(예: `connect-go`)를 통해서만 독립 QUIC 스트림으로 개별 RPC를 매핑해야 한다.
|
||||||
|
|
||||||
|
아래는 다이얼 실패 감지 및 `tls.Config` 분리 복제(`Clone`)가 적용된 트랜스포트 셀렉터 구현 명세이다:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package gateway
|
package gateway
|
||||||
@@ -571,62 +574,53 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/quic-go/quic-go"
|
"github.com/quic-go/quic-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FallbackDialer는 QUIC 연결 시도 실패 시 TCP/HTTP2로 전환하는 다이얼러이다.
|
// TransportKind는 연결에 사용될 최적의 전송 방식을 정의한다.
|
||||||
type FallbackDialer struct {
|
type TransportKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransportQUIC TransportKind = iota // UDP/QUIC (HTTP/3)
|
||||||
|
TransportH2 // TCP/HTTP2 (폴백)
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransportSelector는 연결 수립 전 UDP/QUIC의 가용성을 사전에 프로브하는 셀렉터이다.
|
||||||
|
type TransportSelector struct {
|
||||||
quicTLSConfig *tls.Config // QUIC 전용 ALPN (h3)
|
quicTLSConfig *tls.Config // QUIC 전용 ALPN (h3)
|
||||||
tcpTLSConfig *tls.Config // TCP 전용 ALPN (h2)
|
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFallbackDialer(baseConfig *tls.Config, timeout time.Duration) *FallbackDialer {
|
func NewTransportSelector(baseConfig *tls.Config, timeout time.Duration) *TransportSelector {
|
||||||
// 호출자 tls.Config의 사이드 이펙트 방지를 위해 복제(Clone) 수행
|
// 호출자 tls.Config의 사이드 이펙트 방지를 위해 복제(Clone) 수행
|
||||||
quicTLS := baseConfig.Clone()
|
quicTLS := baseConfig.Clone()
|
||||||
quicTLS.NextProtos = []string{"h3"} // QUIC은 h3 프로토콜에 한정
|
quicTLS.NextProtos = []string{"h3"} // QUIC은 h3 프로토콜에 한정
|
||||||
|
|
||||||
tcpTLS := baseConfig.Clone()
|
return &TransportSelector{
|
||||||
tcpTLS.NextProtos = []string{"h2"} // TCP/HTTP2는 h2 프로토콜에 한정
|
|
||||||
|
|
||||||
return &FallbackDialer{
|
|
||||||
quicTLSConfig: quicTLS,
|
quicTLSConfig: quicTLS,
|
||||||
tcpTLSConfig: tcpTLS,
|
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext는 UDP/QUIC 다이얼을 시도하고 실패 시 0-RTT 이내에 TCP 기반 HTTP/2 연결로 폴백한다.
|
// Probe는 UDP/QUIC 다이얼을 시도하여 가용성을 판정하고, 즉시 연결을 해제한 뒤 적절한 TransportKind를 반환한다.
|
||||||
func (d *FallbackDialer) DialContext(ctx context.Context, addr string) (net.Conn, error) {
|
// (성공 시 커넥션을 즉시 닫고 TransportQUIC 반환. 0-RTT/세션 재개 각주 및 프로브 커넥션 캐싱 변형 병기 가능)
|
||||||
quicCtx, cancel := context.WithTimeout(ctx, d.timeout)
|
func (s *TransportSelector) Probe(ctx context.Context, addr string) (TransportKind, error) {
|
||||||
|
probeCtx, cancel := context.WithTimeout(ctx, s.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// 1. QUIC(HTTP/3) Dial 시도
|
// 1. QUIC(HTTP/3) Dial 프로브 시도
|
||||||
// 각 RPC 호출마다 개별 스트림을 매핑하는 connect-go 또는 커스텀 Transport 구조의 세션을 관리
|
conn, err := quic.DialAddr(probeCtx, addr, s.quicTLSConfig, nil)
|
||||||
quicConn, err := quic.DialAddr(quicCtx, addr, d.quicTLSConfig, nil)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// 단일 커넥션 내부 스트림 생성을 래핑하는 커넥션 핸들러 반환
|
// 프로브 성공 후 커넥션 종료 (HTTP/3 RoundTripper에서 정식으로 독립 연결을 맺고 세션을 관리하도록 함)
|
||||||
return &quicSessionWrapper{conn: quicConn}, nil
|
conn.CloseWithError(0, "probe finished")
|
||||||
|
return TransportQUIC, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. QUIC 다이얼 실패 또는 UDP 블로킹 감지 시 TCP/HTTP/2 Fallback 실행
|
// 2. 다이얼 실패 시 h2 폴백 지시
|
||||||
dialer := &net.Dialer{Timeout: d.timeout}
|
return TransportH2, nil
|
||||||
tcpConn, err := dialer.DialContext(ctx, "tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("both QUIC dial and HTTP/2 fallback failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConn := tls.Client(tcpConn, d.tcpTLSConfig)
|
|
||||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
|
||||||
tcpConn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -690,6 +684,13 @@ func (s *GatewayServer) Start() error {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### 2.1. 전송 계층 단일화 대안 및 런타임 강등 정책
|
||||||
|
|
||||||
|
- **M3-alt 대안: connect-go + http2.Transport 기반 폴백 단일화**:
|
||||||
|
기본 구현은 QUIC 경로에는 `connect-go`를, 폴백 TCP 경로에는 표준 `grpc-go`를 분리 적용하여 구현하나, 설계 대안으로 **폴백 TCP 경로마저 connect-go + http2.Transport 스택으로 단일화**하는 방안을 고려할 수 있다. 이 경우 클라이언트 스택은 `http.Client` 하나로 완전히 단일화되어 `connectT1Client` 1종만 구현하면 되며, 서버 측의 단일 공용 HTTP 핸들러 공유 구조와 구조적인 대칭성을 완벽히 만족하게 된다.
|
||||||
|
- **M4 정책: 런타임 강등 및 복구 정책**:
|
||||||
|
T1 클라이언트 기동 이후 연속 3회 이상 네트워크 타임아웃 또는 전송 계층 에러가 검출될 경우, 클라이언트는 현재 커넥션을 파기하고 즉시 `TransportSelector.Probe`를 재수수행하여 네트워크 가용성을 다시 판정한 후 T1Client 인스턴스를 재생성한다. 단, 이 과정에서의 기존 실행 중인 RPC 세션 정보 복구 및 상태 동기화의 상세 알고리즘은 향후 연구 범위로 위임한다.
|
||||||
|
|
||||||
##### 3. 소거 실험용 비교군 명세 (Naive Single-Stream Tunneling)
|
##### 3. 소거 실험용 비교군 명세 (Naive Single-Stream Tunneling)
|
||||||
|
|
||||||
스트림 매핑 유무가 HOLB 해소에 미치는 실질적인 영향을 입증하는 소거 실험(Ablation Study) 비교군 구성을 위해, 기존에 논의된 단일 QUIC 스트림 고정 터널링(Naive) 코드를 아래와 같이 별도로 명세한다. 이 명세는 평가 단계(Evaluation)에서 패킷 손실 시 TCP와 동등한 수준의 지연시간 병목이 유발됨을 입증하는 대조군으로 기능한다.
|
스트림 매핑 유무가 HOLB 해소에 미치는 실질적인 영향을 입증하는 소거 실험(Ablation Study) 비교군 구성을 위해, 기존에 논의된 단일 QUIC 스트림 고정 터널링(Naive) 코드를 아래와 같이 별도로 명세한다. 이 명세는 평가 단계(Evaluation)에서 패킷 손실 시 TCP와 동등한 수준의 지연시간 병목이 유발됨을 입증하는 대조군으로 기능한다.
|
||||||
@@ -1488,23 +1489,70 @@ func (c *quicStreamConn) SetDeadline(t interface{}) error { return nil }
|
|||||||
func (c *quicStreamConn) SetReadDeadline(t interface{}) error { return nil }
|
func (c *quicStreamConn) SetReadDeadline(t interface{}) error { return nil }
|
||||||
func (c *quicStreamConn) SetWriteDeadline(t interface{}) error { return nil }
|
func (c *quicStreamConn) SetWriteDeadline(t interface{}) error { return nil }
|
||||||
|
|
||||||
// T1Client는 T1 클라우드에서 T2 게이트웨이로 연결하는 gRPC 클라이언트다.
|
// TransportKind는 감지된 전송 프로토콜 종류를 나타낸다.
|
||||||
type T1Client struct {
|
type TransportKind int
|
||||||
conn *grpc.ClientConn
|
|
||||||
agentClient agentv1.AgentServiceClient
|
const (
|
||||||
telemetryClient telemetryv1.TelemetryServiceClient
|
TransportQUIC TransportKind = iota
|
||||||
|
TransportH2
|
||||||
|
)
|
||||||
|
|
||||||
|
// T1Client는 T1 클라우드에서 T2 게이트웨이로 연결하는 gRPC 클라이언트 인터페이스이다.
|
||||||
|
type T1Client interface {
|
||||||
|
ExecuteTask(ctx context.Context, task *agentv1.AgentTask) (*agentv1.AgentResult, error)
|
||||||
|
StreamTelemetry(ctx context.Context, query *telemetryv1.TelemetryQuery, handler func(*telemetryv1.DataPoint) error) error
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewT1Client는 T2 게이트웨이에 gRPC over QUIC 연결을 수립한다.
|
// NewT1Client는 TransportSelector를 통해 QUIC 가용성을 우선 프로브하고 최적의 클라이언트 스택을 구성한다.
|
||||||
func NewT1Client(ctx context.Context, gatewayAddr string, tlsConfig *tls.Config) (*T1Client, error) {
|
func NewT1Client(ctx context.Context, gatewayAddr string, tlsConfig *tls.Config) (T1Client, error) {
|
||||||
dialer := NewQUICDialer(tlsConfig)
|
// T1 클라이언트 수립 시, QUIC 경로에서는 grpc.Dial 및 WithContextDialer 사용이 구조적으로 금지된다.
|
||||||
|
selector := NewTransportSelector(tlsConfig, 3*time.Second)
|
||||||
|
kind, err := selector.Probe(ctx, gatewayAddr)
|
||||||
|
if err != nil {
|
||||||
|
kind = TransportH2
|
||||||
|
}
|
||||||
|
|
||||||
conn, err := grpc.DialContext(
|
if kind == TransportQUIC {
|
||||||
ctx,
|
// (a) 권장 구현 — connect-go + HTTP/3 RoundTripper 구성 (grpc.ClientConn 미사용)
|
||||||
gatewayAddr,
|
quicTLS := tlsConfig.Clone()
|
||||||
grpc.WithContextDialer(dialer.DialContext),
|
quicTLS.NextProtos = []string{"h3"}
|
||||||
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
h3RoundTripper := &http3.RoundTripper{
|
||||||
// Retry policy: 무선 손실 시 지수 백오프
|
TLSClientConfig: quicTLS,
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: h3RoundTripper,
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &connectT1Client{
|
||||||
|
httpClient: httpClient,
|
||||||
|
agentClient: agentv1.NewAgentServiceClient(httpClient),
|
||||||
|
telemetryClient: telemetryv1.NewTelemetryServiceClient(httpClient),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// (b) 폴백 구현 — grpc-go over HTTP/2 구성 (TCP 전용 다이얼러 적용)
|
||||||
|
tcpTLS := tlsConfig.Clone()
|
||||||
|
tcpTLS.NextProtos = []string{"h2"}
|
||||||
|
|
||||||
|
dialer := func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
|
d := &net.Dialer{Timeout: 3 * time.Second}
|
||||||
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConn := tls.Client(conn, tcpTLS)
|
||||||
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(ctx, gatewayAddr,
|
||||||
|
grpc.WithContextDialer(dialer),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()), // dialer단에서 tls 핸드셰이크 처리 완료
|
||||||
grpc.WithDefaultServiceConfig(`{
|
grpc.WithDefaultServiceConfig(`{
|
||||||
"methodConfig": [{
|
"methodConfig": [{
|
||||||
"name": [{}],
|
"name": [{}],
|
||||||
@@ -1519,23 +1567,28 @@ func NewT1Client(ctx context.Context, gatewayAddr string, tlsConfig *tls.Config)
|
|||||||
}`),
|
}`),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("grpc dial: %w", err)
|
return nil, fmt.Errorf("tcp fallback dial failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &T1Client{
|
return &grpcT1Client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
agentClient: agentv1.NewAgentServiceClient(conn),
|
agentClient: agentv1.NewAgentServiceClient(conn),
|
||||||
telemetryClient: telemetryv1.NewTelemetryServiceClient(conn),
|
telemetryClient: telemetryv1.NewTelemetryServiceClient(conn),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteTask는 T2 게이트웨이에 AgentTask를 전송한다.
|
// connectT1Client는 connect-go/HTTP3 기반의 T1Client 구현체이다. (RPC 단위 독립 QUIC 스트림 매핑)
|
||||||
func (c *T1Client) ExecuteTask(ctx context.Context, task *agentv1.AgentTask) (*agentv1.AgentResult, error) {
|
type connectT1Client struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
agentClient agentv1.AgentServiceClient
|
||||||
|
telemetryClient telemetryv1.TelemetryServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectT1Client) ExecuteTask(ctx context.Context, task *agentv1.AgentTask) (*agentv1.AgentResult, error) {
|
||||||
return c.agentClient.Execute(ctx, task)
|
return c.agentClient.Execute(ctx, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamTelemetry는 T2 게이트웨이에서 텔레메트리 스트림을 구독한다.
|
func (c *connectT1Client) StreamTelemetry(
|
||||||
func (c *T1Client) StreamTelemetry(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
query *telemetryv1.TelemetryQuery,
|
query *telemetryv1.TelemetryQuery,
|
||||||
handler func(*telemetryv1.DataPoint) error,
|
handler func(*telemetryv1.DataPoint) error,
|
||||||
@@ -1544,7 +1597,6 @@ func (c *T1Client) StreamTelemetry(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stream start: %w", err)
|
return fmt.Errorf("stream start: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
dp, err := stream.Recv()
|
dp, err := stream.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1556,8 +1608,47 @@ func (c *T1Client) StreamTelemetry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close는 gRPC 연결을 종료한다.
|
func (c *connectT1Client) Close() error {
|
||||||
func (c *T1Client) Close() error { return c.conn.Close() }
|
if transport, ok := c.httpClient.Transport.(*http3.RoundTripper); ok {
|
||||||
|
return transport.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// grpcT1Client는 gRPC-Go/HTTP2 기반의 T1Client 폴백 구현체이다.
|
||||||
|
type grpcT1Client struct {
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
agentClient agentv1.AgentServiceClient
|
||||||
|
telemetryClient telemetryv1.TelemetryServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcT1Client) ExecuteTask(ctx context.Context, task *agentv1.AgentTask) (*agentv1.AgentResult, error) {
|
||||||
|
return c.agentClient.Execute(ctx, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcT1Client) StreamTelemetry(
|
||||||
|
ctx context.Context,
|
||||||
|
query *telemetryv1.TelemetryQuery,
|
||||||
|
handler func(*telemetryv1.DataPoint) error,
|
||||||
|
) error {
|
||||||
|
stream, err := c.telemetryClient.Stream(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stream start: %w", err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
dp, err := stream.Recv()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stream recv: %w", err)
|
||||||
|
}
|
||||||
|
if err := handler(dp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcT1Client) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user