From 6951a29291d897ab3279d110493a29da42bfef0d Mon Sep 17 00:00:00 2001 From: aptend Date: Tue, 14 Jul 2026 16:14:01 +0800 Subject: [PATCH 1/3] fix: reuse CN pipeline connections after clean teardown Negotiate FIN/FIN_ACK before retiring morpc stream state, coordinate server cleanup with client reuse, and add rollback metrics plus unhappy-path acceptance coverage. --- .../cn_pipeline_connection_lifecycle.md | 801 ++++++++++++++++ pkg/cnservice/server.go | 23 +- pkg/cnservice/server_test.go | 31 + pkg/cnservice/types.go | 7 + pkg/common/morpc/metrics.go | 6 + pkg/common/morpc/server.go | 89 +- pkg/common/morpc/server_test.go | 124 ++- pkg/common/morpc/types.go | 24 + pkg/common/runtime/types.go | 4 + pkg/pb/pipeline/pipeline.pb.go | 906 ++++++++++-------- pkg/sql/compile/remoterunClient.go | 200 +++- pkg/sql/compile/remoterunClient_test.go | 205 ++++ pkg/sql/compile/remoterunServer.go | 79 +- pkg/sql/compile/remoterun_lifecycle.go | 163 ++++ pkg/sql/compile/remoterun_lifecycle_test.go | 59 ++ pkg/sql/compile/remoterun_test.go | 15 +- pkg/sql/compile/scope.go | 17 +- pkg/sql/compile/scope_test.go | 4 +- pkg/util/metric/v2/metrics.go | 4 + pkg/util/metric/v2/morpc.go | 12 + pkg/util/metric/v2/pipeline.go | 25 + proto/pipeline.proto | 12 + 22 files changed, 2355 insertions(+), 455 deletions(-) create mode 100644 docs/design/cn_pipeline_connection_lifecycle.md create mode 100644 pkg/sql/compile/remoterun_lifecycle.go create mode 100644 pkg/sql/compile/remoterun_lifecycle_test.go diff --git a/docs/design/cn_pipeline_connection_lifecycle.md b/docs/design/cn_pipeline_connection_lifecycle.md new file mode 100644 index 0000000000000..34af82288742d --- /dev/null +++ b/docs/design/cn_pipeline_connection_lifecycle.md @@ -0,0 +1,801 @@ +# CN Pipeline Connection Lifecycle and Ephemeral-Port Exhaustion + +- Status: Implemented and locally validated +- Tracking issue: [matrixorigin/matrixone#25690](https://github.com/matrixorigin/matrixone/issues/25690) +- Issue baseline commit: `3728f38` +- Last updated: 2026-07-14 + +## 1. Summary + +Sustained distributed queries can exhaust the ephemeral ports used for CN-to-CN +pipeline traffic. The failure is observable as `EADDRNOTAVAIL` / `cannot assign +requested address`, even though both CN processes remain healthy. + +This is not a classic permanent connection leak. The current code eventually +closes each TCP connection, but it couples logical pipeline termination to TCP +connection destruction. Under a high-fanout shuffle workload, the connection +create/close rate exceeds the kernel's ability to reclaim TCP tuples from +`TIME_WAIT`. The configured application limit of 100,000 backends per remote +host does not protect the much smaller ephemeral-port budget. + +The first repair should separate logical stream cleanup from physical connection +cleanup. A successfully drained pipeline stream should use an explicit FIN/ACK +handshake and return its still-healthy, exclusively locked backend to the pool. +Protocol failures and ambiguous teardown must continue to close the entire +backend connection. + +## 2. Scope + +This document covers: + +- CN-to-CN remote pipeline streams created by `PipelineClient`; +- the client/server protocol used to terminate a logical pipeline stream; +- ownership and reuse of morpc backend connections; +- admission control needed to protect TCP tuple and file-descriptor budgets; +- functional, lifecycle, failure-path, and resource acceptance criteria. + +The first implementation phase does not include: + +- changing SQL semantics or optimizer rules; +- replacing morpc with another RPC framework; +- allowing concurrent pipeline streams to share one backend connection; +- redesigning the complete distributed query scheduler; +- relying on operating-system TCP tuning as the primary repair. + +## 3. Reproduction and Evidence + +Issue #25690 uses two CNs, 16 persistent frontend sessions, and 25 executions +per session of a distributed three-table join. Frontend connections are not +repeatedly opened, so frontend short-connection churn is excluded. + +The issue environment reported approximately 5,800 established CN-to-CN +connections per CN and 4,000-5,000 `TIME_WAIT` sockets after failures. The Linux +ephemeral port range was `32768-60999`, or 28,232 ports. + +The workload was also reproduced locally with two CN services and the complete +dataset from the issue. During the run, the socket state reached: + +| State | Observed count | +| --- | ---: | +| `ESTABLISHED` | 2,773 | +| `TIME_WAIT` | 8,741 | +| `CLOSE_WAIT` | 22 | +| `FIN_WAIT_1` + `FIN_WAIT_2` | 45 | + +The run then produced the same failure signature: + +```text +ERROR 20505 (HY000): cannot connect to backend: +dial tcp4 127.0.0.1:18200: connect: can't assign requested address +``` + +After load stopped, established connections and `TIME_WAIT` entries declined, +and both CNs continued to answer `select 1`. This confirms connection churn and +temporary TCP tuple exhaustion rather than a CN crash or a permanently retained +socket. + +The local reproduction used macOS and a native multi-CN process, so it validates +the mechanism and error signature rather than exact Linux timing or an exact +binary-build equivalence. + +## 4. Current Lifecycle + +The relevant path is: + +```text +remote scope + -> messageSenderOnClient + -> PipelineClient.NewStream + -> morpc client.NewStream(backend, lock=true) + -> one exclusively locked backend/TCP connection + -> remote execution returns End or Error + -> local pipeline cleanup + -> stream.Close(true) + -> backend.Close() + -> TCP close and TIME_WAIT + -> inactive backend GC removes the backend from the pool +``` + +Important code locations: + +- `pkg/cnservice/cnclient/types.go`: `dfMaxSenderNumber = 100000`; +- `pkg/cnservice/cnclient/client.go`: pipeline streams use `lock=true`; +- `pkg/sql/compile/remoterunClient.go`: sender cleanup calls + `streamSender.Close(true)`; +- `pkg/common/morpc/backend.go`: `stream.Close(true)` closes the complete + backend connection; +- `pkg/sql/compile/remoterunServer.go`: after sending the result End message, + the server waits for connection or message-context termination before final + related-pipeline cleanup. + +The final server behavior is an important constraint: receiving a result End +message does not currently prove that all server-side resources associated with +the stream have been released. Therefore changing only `Close(true)` to +`Close(false)` is not safe. + +## 5. Root-Cause Analysis + +### 5.1 Q1: Every creation has a destruction + +Q1 passes for the TCP connection involved in this issue: + +- a remote pipeline creates a stream and backend connection; +- normal and error cleanup reach `messageSenderOnClient.close()`; +- `stream.Close(true)` reaches `backend.Close()`; +- inactive backend GC removes stopped backends from the client pool. + +The kernel may retain the closed tuple in `TIME_WAIT`, but that is expected TCP +behavior and not evidence that the application forgot to close the socket. + +### 5.2 Q2: Every wait has a termination + +The known teardown waits are bounded or context-controlled: + +- stop-response waiting is bounded by 30 seconds; +- stream operations use the query deadline, or a default 24-hour deadline when + the query has no deadline; +- server-side connection/message waits end on connection or context + cancellation. + +Q2 therefore does not establish an infinite wait in the current reproducer. +However, introducing a small connection cap inside an already-running pipeline +DAG can create cyclic resource waits that only terminate through a long query +timeout. Any admission-control change must avoid acquiring scarce connection +slots in the middle of a dependency cycle. + +### 5.3 Q3: Every accumulation has a meaningful upper bound + +Q3 fails at two distinct boundaries. + +First, the configured backend limit is not aligned with the protected resource: + +```text +MaxSenderNumber per remote host = 100000 +Linux reproducer ephemeral ports = 28232 +``` + +Second, a concurrent connection limit does not bound connection churn. If a +slot is returned immediately after TCP close, sustained traffic can reuse the +application slot thousands of times while old tuples remain in `TIME_WAIT`. +The protected quantity is closer to: + +```text +ESTABLISHED + TIME_WAIT budget + in-flight dials + <= safety factor * ephemeral-port count +``` + +The high-fanout query plan amplifies the problem because every concurrently +active remote scope exclusively owns one backend connection. + +## 6. Design Constraints + +1. A logical stream must not be reused until both sides agree that its resources + and routing registrations have been removed. +2. A connection with an ambiguous protocol state must never return to the pool. +3. Pipeline streams must remain exclusive on a backend in the first phase. +4. Cancellation and cleanup communication must be bounded and context-aware. +5. Admission control must not introduce a resource-acquisition cycle inside the + pipeline DAG. +6. The first reuse phase is limited to streams that have consumed either a + successful result End or an explicitly caller-authorized, negotiated + terminal response for a clean local StopSending/retry path. Unrecognized + application error, cancellation, malformed response, and any ambiguous state + retain the legacy connection-close cleanup path. +7. Stream sequence bookkeeping must have a per-stream deletion path; moving + from short-lived to reused TCP connections must not introduce session-scoped + memory growth. +8. Pipeline reassembly caches must be removed when assembly succeeds or fails; + closing a cache object without deleting its session-map entry is not a + complete destruction path. +9. The repair must work safely for normal completion, remote error, local + cancellation, peer disconnect, timeout, partial message delivery, and + caller-authorized clean StopSending/retry termination. + +## 7. Repair Plan + +### Phase 1: Explicit logical stream teardown and safe backend reuse + +Introduce a logical stream teardown protocol for successful completion with +states equivalent to: + +```text +OPEN + -> RESULT_END + -> CLIENT_FIN + -> SERVER_CLEANUP + -> FIN_ACK + -> IDLE_REUSABLE +``` + +The intended sequence is: + +1. Every initial `PipelineMessage` and `PrepareDoneNotifyMessage` requests + FIN/ACK teardown using the same additive protobuf field. For a fragmented + PipelineMessage, every fragment carries the same value and the final assembled + message retains it only if all fragments agree. A legacy server ignores the + unknown field. +2. A capable server records the accepted teardown mode in a server-side stream + lifecycle object before executing the pipeline. +3. Remote execution sends its existing successful result End and echoes the + accepted teardown mode. A legacy server sends End without the echo. +4. Before sending an accepted End, the original CN request worker registers a + lifecycle object keyed by `(ClientSession, streamID)`. The object contains a + FIN signal, cleanup completion barrier, terminal state, and removal callback. + The original worker remains the sole owner of its existing related-pipeline + cleanup and waits for FIN, connection close, message cancellation, or a + bounded server-side FIN timeout. A legacy worker retains the current + wait-for-disconnect behavior. +5. The client consumes the successful End and completes local downstream and + child-pipeline cleanup. It sends FIN only when End echoed FIN support. +6. FIN is handled by a separate CN request worker through a dedicated terminal + dispatch path. It atomically wins the FIN terminal path, signals the original + owner, and waits on the cleanup barrier. It does not enter the generic + request path that automatically appends an End or Error response. +7. The original owner removes the related pipeline/dispatch registration and + publishes cleanup completion. Connection close, message cancellation, and + FIN timeout use the same once-only cleanup path but do not send an ACK. +8. The FIN worker is the request's only response owner. It calls a morpc + terminal-stream API that sends exactly one FIN_ACK as the final response and + atomically retires the stream sequence bookkeeping. A terminal failure closes + the session and emits no generic End/Error response. +9. After receiving FIN_ACK, the client unregisters the logical stream with + `Close(false)`. This unlocks the backend while retaining the TCP connection. + +The morpc request callback is synchronous, but the CN wrapper starts a separate +worker for each assembled request and immediately returns from that callback. +Consequently FIN can already be read while the original CN worker waits. The +required coordination is between the original execution worker and the FIN +worker; PR 1 must not add another unowned async execution layer or allow the FIN +worker to bypass cleanup owned by the original worker. + +Registration occurs before the accepted End is sent. Duplicate lifecycle keys +are protocol errors. If registration or End send fails, the original owner runs +cleanup and removes the lifecycle entry through the non-ACK terminal path. +The server FIN timeout begins only after the accepted End has been successfully +flushed. It uses a dedicated teardown context bounded independently from the +execution context and is also cancelled by session close. + +The same registry and teardown path apply to remote execution streams beginning +with `PipelineMessage` and remote dispatch-notify streams beginning with +`PrepareDoneNotifyMessage`. Notify streams are not optional coverage: shuffle +queries create one for each remote receiver, and leaving them on `Close(true)` +would preserve linear TCP churn even if execution streams were reusable. + +The connection remains exclusive during the stream lifetime: + +```go +client.NewStream(ctx, backend, true) +``` + +This is intentionally different from the historical reuse attempt that combined +`lock=false` with `Close(false)`. Current morpc delivery can block the backend +read loop when one logical stream's bounded receive channel is full. Concurrent +stream multiplexing would therefore introduce head-of-line blocking and can +deadlock cyclic pipeline traffic. + +#### Close disposition + +Replace ambiguous boolean state such as `safeToClose` with a synchronized +teardown state machine. One owner performs final disposition; receive, +cancellation, peer-close, and timeout paths submit events. `POISONED` is +monotonic and a late FIN_ACK cannot turn it back into `REUSABLE`. + +```text +OPEN -> RESULT_END -> FIN_SENT -> FIN_ACKED -> REUSABLE + | | | + +---------+-------------+-----------------> POISONED +``` + +At minimum, distinguish: + +| Outcome | Backend action | +| --- | --- | +| FIN_ACK received after complete server cleanup | Reuse with `Close(false)` | +| Peer does not echo FIN support in successful End | Legacy `Close(true)` | +| No pipeline message was successfully sent | Conservative `Close(true)` in PR 1 | +| Remote application error | `Close(true)` in PR 1 | +| Query cancellation or early-stop without a negotiated terminal response | `Close(true)` in PR 1 | +| Caller-authorized StopSending/retry followed by negotiated terminal response and FIN_ACK | Reuse with `Close(false)` | +| FIN or FIN_ACK timeout | Poison and `Close(true)` | +| Send/receive/network error | Poison and `Close(true)` | +| Sequence violation or malformed message | Poison and `Close(true)` | +| Peer closed the connection | Do not reuse | + +PR 1 reuses successful completions and two existing clean local control paths: +StopSending completion and the expected `remote receiver is not registered yet` +notify retry. These paths are reusable only when the initiating caller explicitly +authorizes cleanup, the server returns a per-stream negotiated terminal response, +and FIN_ACK follows the common completion barrier. Arbitrary application errors +and user cancellation remain ineligible. Reuse for those cases requires a +separate ABORT state machine and is explicitly out of scope for PR 1. + +#### Server ordering requirement + +FIN_ACK must be emitted after cleanup, not merely after FIN reception. The ACK +must establish this invariant: + +```text +After FIN_ACK(streamID), the server will not read, write, cancel, or look up any +state belonging to streamID through the old logical stream. +``` + +FIN is not retransmitted on the same connection. TCP already provides ordered, +reliable delivery while the connection remains healthy. If FIN send completion +or FIN_ACK is uncertain, the client poisons and closes the backend. Server-side +cleanup notification remains internally idempotent so FIN racing with session +close cannot run destructors twice; a duplicate wire FIN is not required to +preserve connection reuse. + +#### Server lifecycle ownership + +The lifecycle registry has one entry per FIN-negotiated active stream. Its state +machine has two competing terminal triggers: + +```text +ACTIVE -> RESULT_SENT -> FIN_RECEIVED -> CLEANED -> ACKED -> REMOVED + \-> SESSION_OR_CONTEXT_CLOSE -> CLEANED -> REMOVED + \-> FIN_TIMEOUT -> CLEANED -> SESSION_CLOSED -> REMOVED +``` + +Exactly one trigger wins through CAS or an equivalent mutex transition. The +original request worker owns `RemoveRelatedPipeline` and publishes `CLEANED`; +the FIN worker is the only ACK owner and may ACK only after observing `CLEANED`. +The session/context-close path never ACKs. Every terminal path removes the +registry entry, and cleanup is protected by `sync.Once` or an equivalent +once-only primitive. + +FIN has a dedicated CN dispatch branch. The existing common response logic must +explicitly exclude FIN, as it already excludes the one-way StopSending method. +After `FinishStream` sends FIN_ACK, no `sendEndMessage` or `sendError` call may +follow for the FIN request. + +The existing session-close watcher must signal these lifecycle entries as well +as the existing running-pipeline records. A client that crashes after End must +therefore wake the original owner, run cleanup, unblock any FIN waiter, and +remove the registry entry. A bounded server FIN timeout closes a peer that +negotiated FIN but neither finishes teardown nor disconnects. + +#### Atomic morpc stream retirement + +morpc currently keeps the last received and sent sequence for every stream ID in +connection-scoped containers. Those entries were previously bounded by the +short TCP lifetime. The received container is currently an ordinary map whose +ownership assumes IO-goroutine-only access, while CN request workers write +responses concurrently. A FIN worker must not delete these containers directly. + +PR 1 adds a thread-safe `ClientSession` terminal primitive equivalent to +`FinishStream(ctx, terminalToken, finalResponse)`, where `terminalToken` is an +opaque proof produced when the morpc IO path accepts the FIN sequence. The +implementation owns a morpc +synchronization boundary shared with request-sequence validation and performs: + +1. confirm that the already-validated FIN token is still the current terminal + request; this must not increment or validate FIN as if a second request had + arrived; +2. verify that the pipeline lifecycle completion barrier has already completed; +3. freeze the FIN_ACK stream header using the existing sent-stream sequence; +4. synchronously write and flush FIN_ACK; +5. delete both received and sent sequence entries before releasing the same + synchronization boundary used by the next request validation; +6. if ACK write/flush fails or its delivery becomes uncertain, close the session + through the failure path so all connection-scoped bookkeeping is reclaimed. + +Although the client can receive FIN_ACK before deletion finishes, a subsequent +request on the reused backend cannot validate until `FinishStream` releases the +synchronization boundary. This establishes ACK-before-reuse ordering without a +concurrent map read/write. The API, including its failure-close behavior, must be +safe to call from a CN worker even though `ClientSession` is otherwise documented +as not thread-safe. + +Stream IDs must not be reused during a backend connection's lifetime. A repeated +or late FIN after terminal bookkeeping deletion is a protocol violation and +closes the backend rather than recreating an unbounded tombstone. Acceptance +tests must prove that sequence container size returns to the number of active +streams after repeated reuse. + +#### Pipeline reassembly cache ownership + +Every `Status_Last` PipelineMessage currently enters the assembly path, including +an unfragmented message. `CreateCache(streamID)` stores a cache in the morpc +session map, while `cache.Close()` alone does not delete that map entry. A reused +connection would therefore turn completed streams into O(N) closed cache state. + +The assembly path must call `ClientSession.DeleteCache(streamID)` on every +terminal branch: + +- successful unfragmented assembly; +- successful fragmented assembly; +- fragment teardown-mode mismatch; +- cache pop or assembly error; +- connection/context cancellation or timeout while fragments are pending. The + periodic/session cleanup path must close and delete the cache, not merely drop + the map key. + +Deletion belongs at assembly completion rather than normal FIN because the cache +is no longer needed once the request has been reconstructed. `FinishStream` +provides a final invariant check that no cache remains for the terminal stream; +if residual state cannot be safely removed or proves an ownership violation, it +poisons and closes the session instead of ACKing reuse. + +#### Mixed-version compatibility + +FIN is a new wire-level method. Negotiation is per logical stream and occurs +inside existing backward-compatible protobuf messages: + +- the initial request includes a requested teardown mode; +- both `PipelineMessage` and `PrepareDoneNotifyMessage` carry the request; +- all fragments carry the same requested mode; mismatch is a protocol error; +- a capable server records that mode before execution and echoes it in the + terminal End response; +- the client sends FIN only after observing the echo; +- a legacy server ignores the additive request field, keeps waiting for + disconnect, and returns End without an echo; the client then uses + `Close(true)`. + +This avoids relying on a potentially stale service-discovery capability cache +and ensures the server knows, before its first handler returns, whether it must +transfer lifecycle ownership or retain legacy disconnect cleanup. + +The required behavior is: + +| Client | Server | Behavior | +| --- | --- | --- | +| FIN-capable | FIN-capable | Use FIN/ACK and reuse a clean backend | +| FIN-capable | Legacy or capability unknown | Use legacy `Close(true)` | +| Legacy | FIN-capable | Server preserves the legacy disconnect cleanup path | + +The accepted mode is immutable for that logical stream. A client must not infer +support from a previous stream on the same address or backend; every new stream +requires an echoed acceptance. If a peer is replaced or downgraded, the next +stream naturally falls back to legacy close without a stale-capability window. + +### Phase 2: Resource-budget guardrails + +Add protection at both the per-remote endpoint and local CN levels: + +- maximum active backends per remote endpoint; +- maximum active pipeline backends per local CN; +- context-aware wait queues with cancellation; +- a per-endpoint dial token bucket or equivalent rate limit; +- bounded exponential backoff for connection failures; +- a safety reserve for non-pipeline traffic and recovery operations. + +The active-backend limit and dial-rate limit are separate controls. The former +protects file descriptors, memory, and goroutines; the latter protects ephemeral +ports and `TIME_WAIT` capacity. + +Do not simply change the default from 100,000 to a small constant. If a stream +inside the running DAG blocks while holding other pipeline resources, a small +limit can convert port exhaustion into a distributed query deadlock. + +### Phase 3: Query-boundary admission + +Before launching a distributed query, estimate the maximum concurrent outbound +remote streams per target CN. Acquire the required leases in a deterministic +endpoint order. If all leases cannot be acquired, release partial reservations +and wait or fail according to the query context. + +Moving admission to the query boundary prevents a query from entering the DAG +with only a partial set of the connection resources needed to make progress. +This phase requires a separate design review because nested remote scheduling and +multi-query fairness must be included in the model. + +### Phase 4: Connection-amplification reduction + +The first three phases retain one active TCP connection per active remote stream. +Longer term, reduce the number of physical connections per query/CN pair. + +Any multiplexed design must provide: + +- per-stream credit or flow control; +- a non-blocking connection read loop; +- fair scheduling across logical streams; +- bounded per-stream and per-connection memory; +- reserved progress for FIN, ACK, cancellation, and other control frames; +- explicit handling of connection-level head-of-line blocking. + +Potential transports include a redesigned morpc multiplexer or a transport with +native independent streams such as QUIC. This is not required for resolving the +first-phase lifecycle bug. + +## 8. Alternatives Considered + +| Alternative | Decision | Reason | +| --- | --- | --- | +| Change only `Close(true)` to `Close(false)` | Reject | Server cleanup currently depends on connection/context termination | +| Set `lock=false` and multiplex existing morpc streams | Reject | A full stream channel can block the shared read loop and other streams | +| Only lower `MaxSenderNumber` | Reject as complete fix | Does not bound dial churn and can introduce DAG resource waits | +| Only rate-limit dials | Keep as guardrail | Prevents exhaustion but retains inefficient normal connection churn | +| Expand OS ephemeral port range | Operational mitigation only | Delays failure without correcting lifecycle or admission | +| Enable aggressive TCP reuse or send RST with `SO_LINGER` | Reject | Platform-dependent and can compromise protocol/data reliability | +| Add source IPs to increase tuple space | Reject as primary fix | Operational complexity that masks application resource misuse | + +## 9. Implementation and PR Decomposition + +Keep the initial change reviewable and rollback-safe. + +### PR 1: Protocol and lifecycle + +- add requested/accepted teardown-mode fields plus FIN and FIN_ACK messages; +- carry and validate the requested mode across every fragmented PipelineMessage; +- delete the session reassembly cache on every assembly terminal path; +- negotiate and reuse `PrepareDoneNotifyMessage` streams through the same + lifecycle protocol; +- add a server-side lifecycle registry coordinating the original execution + worker, FIN worker, and session-close path; +- add a dedicated FIN dispatch path with FIN worker as sole response owner; +- make FIN_ACK follow complete server cleanup; +- add atomic morpc `FinishStream`-style ACK/sequence retirement; +- add a synchronized client teardown state machine with monotonic poison state; +- retain `lock=true`; +- reuse successful FIN_ACKed backends and explicitly caller-authorized, + negotiated StopSending/retry terminals after FIN_ACK; +- retain legacy `Close(true)` for unrecognized error, cancellation, unnegotiated + early-stop, legacy peer, and all ambiguous outcomes; +- close the backend on every ambiguous or failed teardown path; +- add unit tests for both protocol endpoints and morpc lock/reuse behavior. + +Likely code areas: + +- `proto/pipeline.proto` and generated `pkg/pb/pipeline` code; +- `pkg/sql/compile/remoterunClient.go`; +- `pkg/sql/compile/remoterunServer.go`; +- `pkg/cnservice/cnclient`; +- `pkg/common/morpc` for terminal stream bookkeeping cleanup; +- targeted tests under the same packages. + +### PR 2: Observability and resource guardrails + +- per-remote active/idle/locked backend gauges; +- backend reuse hit/miss counters; +- backend close counters labeled by reason; +- dial rate, wait duration, waiter count, and admission rejection metrics; +- per-endpoint and global connection limits; +- dial rate limiting and backoff. + +### Separate design/PR: Query admission and multiplexing + +Query-boundary leases and physical-connection coalescing should not be folded +into the lifecycle PR. They have different correctness risks and rollback paths. + +### Implementation decision log + +- Keep `lock=true`: this phase reuses an exclusive backend but does not + multiplex logical streams. +- Enable negotiation by default with `disable-stream-reuse` as the rollback + switch: mixed-version safety is per-stream and a missing acceptance echo + always falls back to `Close(true)`. +- A negotiated terminal application error is not sufficient by itself for + reuse. Only the caller-recognized StopSending and notify-registration retry + paths may authorize FIN after local cleanup; all other errors remain poisoned. +- Hard connection budgets and query-boundary admission remain PR 2 work. The + lifecycle repair removes normal dial churn but does not redefine the existing + maximum-concurrency policy. +- Local acceptance uses separate CN processes. A native combined multi-CN + process shares process-global colexec registries and is not equivalent to the + issue topology. + +## 10. Acceptance Criteria + +Sections 10.1-10.5 are mandatory for PR 1 unless an item is explicitly marked +PR 2. Section 10.6 defines benchmark reporting rather than an environment-neutral +merge gate. Resource admission and hard connection budgets are PR 2 criteria and +must not block independent validation of PR 1. + +### 10.1 Functional correctness + +- Run the exact two-CN reproduction from issue #25690 with 16 persistent + sessions and 25 executions per session. +- All 400 statements must complete successfully. +- Every statement must return `499999`. +- Both CNs must remain healthy and accept new queries throughout and after the + run. +- No `EADDRNOTAVAIL`, backend-connect failure, remote-receiver timeout, or + protocol-sequence error may occur. + +### 10.2 Connection lifecycle + +- After warm-up, repeated sequential executions against the same remote CN must + reuse existing healthy backends. +- Backend connect count must plateau near the workload's concurrent high-water + mark instead of increasing approximately with completed remote scopes. +- A FIN_ACKed stream must leave no server pipeline, dispatch, stream, goroutine, + or session registration associated with its stream ID. +- Lifecycle-registry size must return to zero after FIN completion, FIN timeout, + peer close, message cancellation, registration failure, and End send failure. +- After N sequential successful streams on one reused backend, morpc received and + sent sequence containers must return to zero terminal entries; their size is + bounded by concurrently active streams, not completed streams. +- Session cache size must equal the number of streams currently awaiting more + fragments. It must return to zero after both unfragmented and fragmented + sequential reuse tests and after every assembly error path. +- A backend must not be reused before FIN_ACK. +- A poisoned or ambiguous backend must not return to the pool. +- Idle backend GC must still close genuinely unused connections. + +### 10.3 Resource behavior + +- `TIME_WAIT` must not grow approximately linearly with the number of completed + statements after the pool is warm. +- Established connection count must converge to a stable high-water mark under a + fixed-concurrency workload. +- File descriptor, goroutine, and heap usage must converge after the workload + stops. +- PR 2: no backend pool may grow beyond its configured per-remote or global + budget. +- PR 2: admission waits and dial rates must be visible through metrics. + +No universal absolute socket-count threshold is specified for PR 1 because the +number of active remote edges depends on CN count, DOP, and the query plan. The +key PR 1 invariant is reuse and convergence rather than continued growth. PR 2 +must define deployment defaults and hard resource thresholds. + +### 10.4 Unhappy paths + +Tests must cover at least: + +| Scenario | Expected result | +| --- | --- | +| FIN send completion is uncertain | No retransmit; backend poisoned and closed | +| FIN_ACK is not observed before deadline | Timeout, backend poisoned and closed | +| Duplicate or late FIN | Protocol violation and backend close; no tombstone growth | +| Peer disconnects before FIN_ACK | Backend closed; no reuse | +| Query cancelled before result End | Legacy connection-close cleanup; no reuse | +| Query cancelled after result End but before FIN_ACK | Backend poisoned and closed | +| Server returns an application error | Correct SQL error and `Close(true)`; no reuse in PR 1 | +| Notify receiver is not registered yet and caller retries | Negotiated terminal cleanup, FIN_ACK, and backend reuse before retry | +| Malformed or out-of-order response | Protocol error and backend close | +| Client crashes after result End | Server connection context releases all stream resources | +| Server crashes during teardown | Client wait terminates and connection is removed | +| Legacy server ignores requested teardown mode | End has no acceptance echo; client uses `Close(true)` | +| FIN-capable server receives a legacy request | Server retains wait-for-disconnect cleanup | +| PipelineMessage is split into multiple fragments | Every fragment agrees on requested mode; assembled request preserves it | +| Fragment teardown modes disagree | Protocol error and backend close | +| Unfragmented PipelineMessage assembly completes | Cache entry is deleted immediately | +| Fragmented assembly completes or fails | Cache entry is deleted on every terminal branch | +| PrepareDoneNotifyMessage completes successfully | Accepted FIN/ACK path reuses the exclusive backend | +| Legacy notify client or server | Notify stream falls back to `Close(true)` | +| Notify dispatch returns an error or is cancelled | Correct error and `Close(true)`; no PR 1 reuse | +| FIN succeeds | Exactly one FIN_ACK; no trailing generic End/Error; next stream succeeds | +| Receive channel is full before End | Stream is ineligible for reuse; poison/close terminates cleanup | +| Server sends data after End or FIN_ACK | Protocol violation and backend close | +| PR 2: backend pool is at capacity | Context-aware backpressure; no busy loop | +| PR 2: waiting query is cancelled | Waiter removed promptly; permit accounting remains correct | + +### 10.5 Deadlock and concurrency safety + +- Existing shuffle-join and remote-dispatch regression suites must pass. +- Existing cyclic pipeline dependency tests must pass with `lock=true` and + successful connection reuse enabled. +- PR 2 must add a cyclic pipeline test at the configured connection limit. It + must prove progress or bounded admission failure; a long query timeout is not + accepted as the normal release mechanism. +- Run concurrent stream close, peer close, FIN reception, and context + cancellation under the race detector for the affected pure-Go packages. +- The server lifecycle completion barrier and backend close must be idempotent + when FIN races with connection or context cancellation. +- `FinishStream` must be race-tested against the next stream request on the same + backend and against session close; no concurrent sequence-map access is + permitted. +- Client teardown must end exactly once in `REUSABLE` or `POISONED`; poison must + never be reversed by a late ACK. +- Mixed-version tests must prove per-stream negotiation fallback without using a + cached capability from an earlier stream or CN incarnation. + +### 10.6 Benchmark report + +- Report median and P95 query latency before and after the change on identical + hardware, commit configuration, CN count, DOP, dataset, and query plan. +- Use at least three independent runs, state warm-up count and measurement + window, and report variance. Any merge threshold is agreed from these results + rather than assumed by this design document. +- For a deterministic fixed-concurrency reuse test, once the pool reaches the + required concurrent high-water mark, completing additional sequential waves + must not create backends unless the high-water mark increases or an existing + backend becomes unhealthy. +- Teardown must not add one network round-trip to every returned batch; it occurs + once per logical stream. + +### 10.7 Observability + +At minimum, expose enough information to distinguish: + +- active, locked, idle, stopped, and waiting backends; +- newly created versus reused backends; +- backend closes by normal idle, protocol error, cancellation, peer close, and + FIN/ACK timeout; +- FIN/ACK latency and timeout count; +- dial attempts, failures, and rate-limit waits; +- active logical pipeline streams and server-side stream registrations. + +PR 1 must expose FIN/ACK outcomes, reuse count, close reason, active logical +streams, lifecycle-registry size, and morpc sequence-container size. Admission +wait and resource-budget metrics belong to PR 2. + +Alerts should be based on rates and budget utilization, especially backend dial +rate, connect failures, admission wait latency, and connection-budget occupancy. + +### 10.8 Local validation record (2026-07-14) + +Validation used separate log, TN, CN1, and CN2 processes so process-global CN +registries matched the issue's multi-process topology. The exact issue dataset +and query were run through 16 persistent frontend sessions with 25 executions +per session. + +- Two consecutive 400-statement waves completed with 400/400 correct results; + every result was `499999`. +- CN-to-CN `TIME_WAIT` socket endpoints remained zero throughout both waves. +- Established socket endpoints were 1,548 after warm-up, peaked/ended at 1,636 + in wave 1, and peaked/ended at 1,680 in wave 2. The small second-wave increase + matched its higher observed concurrency high-water mark rather than completed + statement count. +- Both CN frontend ports returned `select 1` after the load. +- After the configured idle-backend interval, established CN pipeline sockets + returned to zero, confirming idle GC still destroys genuinely unused + connections. +- Targeted package tests and race-detector tests for morpc FIN retirement, + lifecycle coordination, and client teardown passed. + +The benchmark-reporting item in 10.6 still requires a controlled before/after +performance environment; it is not claimed by this local correctness record. + +### 10.9 Linux multi-CN validation record (2026-07-14) + +Validation on Debian 13 (`linux/amd64`, kernel 6.12.63) used independent log, +TN, CN1, and CN2 processes. It reused the exact issue dataset and ran two +consecutive waves of 16 persistent frontend sessions with 25 executions per +session. + +- Both waves completed with 400/400 correct results; every result was `499999`. +- CN-to-CN `TIME_WAIT` remained zero during both waves. Established socket + endpoints reached 2,778 in wave 1 and increased only to 2,814 in wave 2, + tracking the concurrent high-water mark rather than the 400 additional + statements. +- No `EADDRNOTAVAIL`, backend-connect failure, remote-receiver timeout, + protocol-sequence error, or panic occurred during the workload. +- After idle GC, established CN pipeline sockets returned from 2,814 to zero + and the two CN processes' combined file-descriptor count fell from about + 2,895 to 63. A post-GC execution through CN2 returned `499999`, and both CN + frontend ports remained healthy. +- Combined CN RSS peaked near 5.5 GB and was about 4.7 GB after the short idle + window. Go heap retention makes this insufficient evidence of a leak, but a + longer soak with heap and goroutine profiles remains necessary for the full + memory-convergence criterion in 10.3. + +## 11. Rollout and Rollback + +Use a feature flag or configuration gate for healthy backend reuse during the +initial rollout. Suggested sequence: + +1. deploy code that understands requested/accepted teardown mode, with clients + not requesting FIN teardown by default; +2. enable FIN requests for a small set of successful streams; verify legacy + responses without an acceptance echo still use disconnect cleanup; +3. verify FIN/ACK cleanup, sequence-container bounds, and completion-barrier + idempotency metrics; +4. enable successful-completion reuse on a small set of CNs; +5. compare connection churn, query latency, and cleanup metrics; +6. expand successful-completion reuse. Error/cancellation reuse remains disabled + until a separately reviewed ABORT protocol exists. + +Rollback first disables new FIN requests. Streams without an accepted teardown +echo use `Close(true)`, so legacy cleanup remains available throughout. Existing +FIN-negotiated streams must drain or have their backends closed before rolling a +server back to a version that does not understand FIN. + +## 12. Open Questions + +- Is the existing result End message allowed to be redefined as proof of complete + server cleanup, or must FIN/ACK remain a distinct phase? The current server + ordering indicates a distinct phase is safer. +- What is the minimal server lifecycle object that can own the execution context, + cleanup closure, completion barrier, and connection-close fallback after the + original request handler returns? +- Should a later ABORT protocol allow reuse after application error or + cancellation, and how will it prove that dispatch writers have stopped? +- How should ephemeral-port budgets be configured across Kubernetes network + namespaces and deployments with multiple source or destination IPs? +- Can the compiler calculate a safe upper bound for remote streams per target CN, + including nested remote execution? +- Which existing cyclic shuffle tests best represent the historical reason that + unrestricted multiplexing was reverted? diff --git a/pkg/cnservice/server.go b/pkg/cnservice/server.go index 7349e27a38a93..729044637b187 100644 --- a/pkg/cnservice/server.go +++ b/pkg/cnservice/server.go @@ -905,9 +905,10 @@ func handleWaitingNextMsg(ctx context.Context, message morpc.Message, cs morpc.C if cache, err = cs.CreateCache(ctx, message.GetID()); err != nil { return err } - cache.Add(message) + return cache.Add(message) + default: + return moerr.NewInvalidInputNoCtx("only pipeline messages may be fragmented") } - return nil } func handleAssemblePipeline(ctx context.Context, message morpc.Message, cs morpc.ClientSession) error { @@ -917,19 +918,27 @@ func handleAssemblePipeline(ctx context.Context, message morpc.Message, cs morpc if err != nil { return err } + // CreateCache also returns a cache for an unfragmented message. Always + // remove the map entry on every terminal assembly path, not just close the + // queue object. + defer cs.DeleteCache(message.GetID()) + finalMessage := message.(*pipeline.Message) for { - msg, ok, err := cache.Pop() + cached, ok, err := cache.Pop() if err != nil { return err } if !ok { - cache.Close() break } - data = append(data, msg.(*pipeline.Message).GetData()...) + fragment, ok := cached.(*pipeline.Message) + if !ok || fragment.GetCmd() != finalMessage.GetCmd() || + fragment.GetRequestedTeardownMode() != finalMessage.GetRequestedTeardownMode() { + return moerr.NewInvalidInputNoCtx("inconsistent pipeline message fragments") + } + data = append(data, fragment.GetData()...) } - msg := message.(*pipeline.Message) - msg.SetData(append(data, msg.GetData()...)) + finalMessage.SetData(append(data, finalMessage.GetData()...)) return nil } diff --git a/pkg/cnservice/server_test.go b/pkg/cnservice/server_test.go index dda4586deb941..e2cdaba5e0d84 100644 --- a/pkg/cnservice/server_test.go +++ b/pkg/cnservice/server_test.go @@ -141,6 +141,7 @@ func Test_InitServer(t *testing.T) { session := mock_morpc.NewMockClientSession(ctrl) msg.Cmd = pipeline.Method_PipelineMessage session.EXPECT().CreateCache(ctx, uint64(0)).Return(&testMessageCache{}, nil).Times(2) + session.EXPECT().DeleteCache(uint64(0)).Times(1) msg.Sid = pipeline.Status_WaitingNext err = srv.handleRequest( @@ -173,6 +174,36 @@ type testMessageCache struct { cache []morpc.Message } +func TestHandleAssemblePipelineDeletesCacheAndRejectsMixedNegotiation(t *testing.T) { + t.Run("assembles and deletes", func(t *testing.T) { + ctrl := gomock.NewController(t) + session := mock_morpc.NewMockClientSession(ctrl) + ctx := context.Background() + cache := &testMessageCache{cache: []morpc.Message{ + &pipeline.Message{Id: 42, Cmd: pipeline.Method_PipelineMessage, Data: []byte("a"), RequestedTeardownMode: pipeline.StreamTeardownMode_FinishAck}, + &pipeline.Message{Id: 42, Cmd: pipeline.Method_PipelineMessage, Data: []byte("b"), RequestedTeardownMode: pipeline.StreamTeardownMode_FinishAck}, + }} + session.EXPECT().CreateCache(ctx, uint64(42)).Return(cache, nil) + session.EXPECT().DeleteCache(uint64(42)) + final := &pipeline.Message{Id: 42, Cmd: pipeline.Method_PipelineMessage, Data: []byte("c"), RequestedTeardownMode: pipeline.StreamTeardownMode_FinishAck} + require.NoError(t, handleAssemblePipeline(ctx, final, session)) + require.Equal(t, []byte("abc"), final.GetData()) + }) + + t.Run("mixed teardown mode is protocol error", func(t *testing.T) { + ctrl := gomock.NewController(t) + session := mock_morpc.NewMockClientSession(ctrl) + ctx := context.Background() + cache := &testMessageCache{cache: []morpc.Message{ + &pipeline.Message{Id: 43, Cmd: pipeline.Method_PipelineMessage, RequestedTeardownMode: pipeline.StreamTeardownMode_LegacyClose}, + }} + session.EXPECT().CreateCache(ctx, uint64(43)).Return(cache, nil) + session.EXPECT().DeleteCache(uint64(43)) + final := &pipeline.Message{Id: 43, Cmd: pipeline.Method_PipelineMessage, RequestedTeardownMode: pipeline.StreamTeardownMode_FinishAck} + require.Error(t, handleAssemblePipeline(ctx, final, session)) + }) +} + func (c *testMessageCache) Add(val morpc.Message) error { c.cache = append(c.cache, val) return nil diff --git a/pkg/cnservice/types.go b/pkg/cnservice/types.go index e4e3bc19e89b3..edf723e70565a 100644 --- a/pkg/cnservice/types.go +++ b/pkg/cnservice/types.go @@ -158,6 +158,9 @@ type Config struct { BatchRows int64 `toml:"batch-rows"` // BatchSize is the memory limit for one batch BatchSize int64 `toml:"batch-size"` + // DisableStreamReuse is an emergency rollback gate for negotiated + // FIN/FIN_ACK pipeline teardown. It is enabled by default when false. + DisableStreamReuse bool `toml:"disable-stream-reuse"` } // Frontend parameters for the frontend @@ -454,6 +457,10 @@ func (c *Config) Validate() error { moruntime.EnableCheckInvalidRCErrors, c.Txn.EnableCheckRCInvalidError, ) + moruntime.ServiceRuntime(c.UUID).SetGlobalVariables( + moruntime.EnablePipelineStreamReuse, + !c.Pipeline.DisableStreamReuse, + ) return nil } diff --git a/pkg/common/morpc/metrics.go b/pkg/common/morpc/metrics.go index a51d81db16475..a6194f7fb4b2b 100644 --- a/pkg/common/morpc/metrics.go +++ b/pkg/common/morpc/metrics.go @@ -130,6 +130,9 @@ type serverMetrics struct { outputBytesCounter prometheus.Counter sendingQueueSizeGauge prometheus.Gauge sessionSizeGauge prometheus.Gauge + receivedStreamStateGauge prometheus.Gauge + sentStreamStateGauge prometheus.Gauge + messageCacheStateGauge prometheus.Gauge sendingBatchSizeGauge prometheus.Gauge writeDurationHistogram prometheus.Observer writeLatencyDurationHistogram prometheus.Observer @@ -144,6 +147,9 @@ func newServerMetrics(name string) *serverMetrics { sendingQueueSizeGauge: v2.NewRPCServerSendingQueueSizeGaugeByName(name), writeLatencyDurationHistogram: v2.NewRPCServerWriteLatencyDurationHistogramByName(name), sessionSizeGauge: v2.NewRPCServerSessionSizeGaugeByName(name), + receivedStreamStateGauge: v2.NewRPCServerStreamStateGaugeByName(name, "received_sequence"), + sentStreamStateGauge: v2.NewRPCServerStreamStateGaugeByName(name, "sent_sequence"), + messageCacheStateGauge: v2.NewRPCServerStreamStateGaugeByName(name, "message_cache"), inputBytesCounter: v2.NewRPCInputCounter(), outputBytesCounter: v2.NewRPCOutputCounter(), } diff --git a/pkg/common/morpc/server.go b/pkg/common/morpc/server.go index 0a2bf8efc5d33..8cdc0f37a17d9 100644 --- a/pkg/common/morpc/server.go +++ b/pkg/common/morpc/server.go @@ -244,12 +244,17 @@ func (s *server) onMessage(rs goetty.IOSession, value any, sequence uint64) erro if request.stream && !cs.validateStreamRequest(requestID, request.streamSequence) { s.logger.Error("failed to handle stream request", - zap.Uint32("last-sequence", cs.receivedStreamSequences[requestID]), + zap.Uint32("last-sequence", cs.lastReceivedStreamSequence(requestID)), zap.Uint32("current-sequence", request.streamSequence), zap.String("client", rs.RemoteAddress())) cs.cancelWrite() return moerr.NewStreamClosedNoCtx() } + if request.stream { + request.Ctx = context.WithValue(request.Ctx, streamTerminalTokenContextKey{}, StreamTerminalToken{ + owner: cs, streamID: requestID, sequence: request.streamSequence, + }) + } // handle internal message if request.internal { @@ -557,7 +562,10 @@ type clientSession struct { conn goetty.IOSession c chan *Future newFutureFunc func() *Future - // streaming id -> last received sequence, no concurrent, access in io goroutine + // streaming id -> last received sequence. FinishStream also accesses this + // map from a handler goroutine, so streamStateMu is the synchronization + // boundary for validation and terminal retirement. + streamStateMu sync.Mutex receivedStreamSequences map[uint64]uint32 // streaming id -> last sent sequence, multi-stream access in multi-goroutines if // the tcp connection is shared. But no concurrent in one stream. @@ -604,6 +612,8 @@ func (cs *clientSession) RemoteAddress() string { } func (cs *clientSession) Close() error { + cs.streamStateMu.Lock() + defer cs.streamStateMu.Unlock() cs.mu.Lock() defer cs.mu.Unlock() if cs.mu.closed { @@ -613,6 +623,15 @@ func (cs *clientSession) Close() error { cs.cleanSend() close(cs.c) cs.mu.closed = true + if cs.metrics != nil { + cs.metrics.receivedStreamStateGauge.Sub(float64(len(cs.receivedStreamSequences))) + sentCount := 0 + cs.sentStreamSequences.Range(func(_, _ any) bool { sentCount++; return true }) + cs.metrics.sentStreamStateGauge.Sub(float64(sentCount)) + cs.metrics.messageCacheStateGauge.Sub(float64(len(cs.mu.caches))) + } + clear(cs.receivedStreamSequences) + cs.sentStreamSequences.Clear() for _, c := range cs.mu.caches { c.cache.Close() } @@ -749,7 +768,11 @@ func (cs *clientSession) checkCacheTimeout() { cs.mu.Lock() for k, c := range cs.mu.caches { if c.closeIfTimeout() { + c.cache.Close() delete(cs.mu.caches, k) + if cs.metrics != nil { + cs.metrics.messageCacheStateGauge.Dec() + } } } cs.mu.Unlock() @@ -766,6 +789,8 @@ func (cs *clientSession) cancelWrite() { func (cs *clientSession) validateStreamRequest( id uint64, sequence uint32) bool { + cs.streamStateMu.Lock() + defer cs.streamStateMu.Unlock() expectSequence := cs.receivedStreamSequences[id] + 1 if sequence != expectSequence { return false @@ -773,10 +798,64 @@ func (cs *clientSession) validateStreamRequest( cs.receivedStreamSequences[id] = sequence if sequence == 1 { cs.sentStreamSequences.Store(id, uint32(0)) + if cs.metrics != nil { + cs.metrics.receivedStreamStateGauge.Inc() + cs.metrics.sentStreamStateGauge.Inc() + } } return true } +func (cs *clientSession) lastReceivedStreamSequence(id uint64) uint32 { + cs.streamStateMu.Lock() + defer cs.streamStateMu.Unlock() + return cs.receivedStreamSequences[id] +} + +// FinishStream synchronously flushes the final response before removing both +// receive and send sequence entries. Holding streamStateMu prevents the IO loop +// from validating a later request against half-retired state. +func (cs *clientSession) FinishStream( + ctx context.Context, + token StreamTerminalToken, + response Message, +) error { + cs.streamStateMu.Lock() + valid := token.owner == cs && + cs.receivedStreamSequences[token.streamID] == token.sequence && + response != nil && response.GetID() == token.streamID + if !valid { + cs.streamStateMu.Unlock() + _ = cs.Close() + return moerr.NewStreamClosedNoCtx() + } + + cache, err := cs.GetCache(token.streamID) + if err != nil || cache != nil { + cs.streamStateMu.Unlock() + _ = cs.Close() + if err != nil { + return err + } + return moerr.NewInternalErrorNoCtx("cannot finish stream with pending message cache") + } + + err = cs.Write(ctx, response) + if err == nil { + delete(cs.receivedStreamSequences, token.streamID) + cs.sentStreamSequences.Delete(token.streamID) + if cs.metrics != nil { + cs.metrics.receivedStreamStateGauge.Dec() + cs.metrics.sentStreamStateGauge.Dec() + } + } + cs.streamStateMu.Unlock() + if err != nil { + _ = cs.Close() + } + return err +} + func (cs *clientSession) CreateCache( ctx context.Context, cacheID uint64) (MessageCache, error) { @@ -791,6 +870,9 @@ func (cs *clientSession) CreateCache( if !ok { v = cacheWithContext{ctx: ctx, cache: newCache()} cs.mu.caches[cacheID] = v + if cs.metrics != nil { + cs.metrics.messageCacheStateGauge.Inc() + } cs.startCheckCacheTimeout() } return v.cache, nil @@ -806,6 +888,9 @@ func (cs *clientSession) DeleteCache(cacheID uint64) { if c, ok := cs.mu.caches[cacheID]; ok { c.cache.Close() delete(cs.mu.caches, cacheID) + if cs.metrics != nil { + cs.metrics.messageCacheStateGauge.Dec() + } } } diff --git a/pkg/common/morpc/server_test.go b/pkg/common/morpc/server_test.go index 8c135853dbb25..7eb1d5a471d40 100644 --- a/pkg/common/morpc/server_test.go +++ b/pkg/common/morpc/server_test.go @@ -26,6 +26,7 @@ import ( "github.com/fagongzi/goetty/v2" "github.com/fagongzi/goetty/v2/buf" + "github.com/matrixorigin/matrixone/pkg/common/moerr" "github.com/matrixorigin/matrixone/pkg/common/stopper" "github.com/matrixorigin/matrixone/pkg/logutil" "github.com/stretchr/testify/assert" @@ -442,6 +443,121 @@ func TestStreamServerWithCache(t *testing.T) { }) } +func TestFinishStreamFlushesAckAndRetiresSequenceState(t *testing.T) { + testRPCServer(t, func(rs *server) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var requestCount atomic.Int32 + retired := make(chan *clientSession, 1) + rs.RegisterRequestHandler(func(ctx context.Context, msg RPCMessage, _ uint64, session ClientSession) error { + cs := session.(*clientSession) + if requestCount.Add(1) == 1 { + return cs.Write(ctx, newTestMessage(msg.Message.GetID())) + } + token, ok := StreamTerminalTokenFromContext(ctx) + if !ok { + return moerr.NewInternalErrorNoCtx("missing terminal token") + } + if err := cs.FinishStream(ctx, token, newTestMessage(msg.Message.GetID())); err != nil { + return err + } + retired <- cs + return nil + }) + + client := newTestClient(t) + defer func() { require.NoError(t, client.Close()) }() + stream, err := client.NewStream(ctx, testAddr, false) + require.NoError(t, err) + defer func() { require.NoError(t, stream.Close(false)) }() + responses, err := stream.Receive() + require.NoError(t, err) + + require.NoError(t, stream.Send(ctx, newTestMessage(stream.ID()))) + select { + case <-responses: + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + require.NoError(t, stream.Send(ctx, newTestMessage(stream.ID()))) + select { + case <-responses: + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + + select { + case cs := <-retired: + cs.streamStateMu.Lock() + _, receivedExists := cs.receivedStreamSequences[stream.ID()] + cs.streamStateMu.Unlock() + _, sentExists := cs.sentStreamSequences.Load(stream.ID()) + require.False(t, receivedExists) + require.False(t, sentExists) + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + }) +} + +func TestFinishStreamPoisonsSessionWithPendingCache(t *testing.T) { + cs := newClientSession(nil, newTestIOSession(nil, nil), nil, func() *Future { return &Future{} }, nil) + cs.receivedStreamSequences[11] = 2 + cs.sentStreamSequences.Store(uint64(11), uint32(1)) + _, err := cs.CreateCache(context.Background(), 11) + require.NoError(t, err) + token := StreamTerminalToken{owner: cs, streamID: 11, sequence: 2} + err = cs.FinishStream(context.Background(), token, newTestMessage(11)) + require.Error(t, err) + cs.mu.RLock() + require.True(t, cs.mu.closed) + cs.mu.RUnlock() +} + +func TestFinishStreamRacesWithSessionClose(t *testing.T) { + testRPCServer(t, func(rs *server) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + done := make(chan struct{}) + rs.RegisterRequestHandler(func(ctx context.Context, msg RPCMessage, _ uint64, session ClientSession) error { + cs := session.(*clientSession) + token, ok := StreamTerminalTokenFromContext(ctx) + if !ok { + return moerr.NewInternalErrorNoCtx("missing terminal token") + } + start := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + <-start + _ = cs.FinishStream(ctx, token, newTestMessage(msg.Message.GetID())) + }() + go func() { + defer wg.Done() + <-start + _ = cs.Close() + }() + close(start) + wg.Wait() + close(done) + return nil + }) + + client := newTestClient(t) + defer func() { require.NoError(t, client.Close()) }() + stream, err := client.NewStream(ctx, testAddr, false) + require.NoError(t, err) + require.NoError(t, stream.Send(ctx, newTestMessage(stream.ID()))) + select { + case <-done: + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + }) +} + func TestServerTimeoutCacheWillRemoved(t *testing.T) { testRPCServer(t, func(rs *server) { ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10) @@ -452,14 +568,14 @@ func TestServerTimeoutCacheWillRemoved(t *testing.T) { assert.NoError(t, c.Close()) }() - cc := make(chan struct{}) + cc := make(chan MessageCache, 1) rs.RegisterRequestHandler(func(ctx context.Context, msg RPCMessage, seq uint64, cs ClientSession) error { request := msg.Message cache, err := cs.CreateCache(ctx, request.GetID()) if err != nil { return err } - close(cc) + cc <- cache return cache.Add(request) }) @@ -471,7 +587,7 @@ func TestServerTimeoutCacheWillRemoved(t *testing.T) { // Stream.Send requires request.GetID() == stream.ID(); stream id is assigned by backend at NewStream(). assert.NoError(t, st.Send(ctx, newTestMessage(st.ID()))) - <-cc + cache := <-cc v, ok := rs.sessions.Load(uint64(1)) if ok { cs := v.(*clientSession) @@ -479,6 +595,8 @@ func TestServerTimeoutCacheWillRemoved(t *testing.T) { cs.mu.RLock() if len(cs.mu.caches) == 0 { cs.mu.RUnlock() + _, err := cache.Len() + require.Error(t, err, "expired cache must be closed before removal") return } cs.mu.RUnlock() diff --git a/pkg/common/morpc/types.go b/pkg/common/morpc/types.go index 82c36abaac7a9..1af68617bf55b 100644 --- a/pkg/common/morpc/types.go +++ b/pkg/common/morpc/types.go @@ -75,6 +75,30 @@ type RPCMessage struct { createAt time.Time } +// StreamTerminalToken proves that the server IO loop validated a particular +// stream request. Its fields are intentionally private so application handlers +// can pass, but cannot manufacture, terminal authority. +type StreamTerminalToken struct { + owner *clientSession + streamID uint64 + sequence uint32 +} + +type streamTerminalTokenContextKey struct{} + +// StreamTerminalTokenFromContext returns the token attached to a validated +// streaming request. Non-streaming requests do not carry one. +func StreamTerminalTokenFromContext(ctx context.Context) (StreamTerminalToken, bool) { + token, ok := ctx.Value(streamTerminalTokenContextKey{}).(StreamTerminalToken) + return token, ok && token.owner != nil +} + +// StreamFinisher is implemented by server-side sessions that can synchronously +// flush a final response and atomically retire the stream sequence state. +type StreamFinisher interface { + FinishStream(context.Context, StreamTerminalToken, Message) error +} + // InternalMessage returns true means the rpc message is the internal message in morpc. func (m RPCMessage) InternalMessage() bool { return m.internal diff --git a/pkg/common/runtime/types.go b/pkg/common/runtime/types.go index cd4383dc4a74d..50f5b312d4dd0 100644 --- a/pkg/common/runtime/types.go +++ b/pkg/common/runtime/types.go @@ -58,6 +58,10 @@ const ( BackgroundCNSelector = "background-cn-selector" PipelineClient = "pipeline-client" + // EnablePipelineStreamReuse controls FIN/FIN_ACK teardown for clean remote + // pipeline streams. Setting it to false provides a rolling rollback gate; + // absence defaults to enabled. + EnablePipelineStreamReuse = "enable-pipeline-stream-reuse" // TestingContextKey is the key of context for testing TestingContextKey = "testing-context" diff --git a/pkg/pb/pipeline/pipeline.pb.go b/pkg/pb/pipeline/pipeline.pb.go index c9048b7e4fbfc..cb4ef546199ae 100644 --- a/pkg/pb/pipeline/pipeline.pb.go +++ b/pkg/pb/pipeline/pipeline.pb.go @@ -37,6 +37,8 @@ const ( Method_BatchMessage Method = 2 Method_PrepareDoneNotifyMessage Method = 3 Method_StopSending Method = 4 + Method_PipelineStreamFinish Method = 5 + Method_PipelineStreamFinishAck Method = 6 ) var Method_name = map[int32]string{ @@ -45,6 +47,8 @@ var Method_name = map[int32]string{ 2: "BatchMessage", 3: "PrepareDoneNotifyMessage", 4: "StopSending", + 5: "PipelineStreamFinish", + 6: "PipelineStreamFinishAck", } var Method_value = map[string]int32{ @@ -53,6 +57,8 @@ var Method_value = map[string]int32{ "BatchMessage": 2, "PrepareDoneNotifyMessage": 3, "StopSending": 4, + "PipelineStreamFinish": 5, + "PipelineStreamFinishAck": 6, } func (x Method) String() string { @@ -63,6 +69,34 @@ func (Method) EnumDescriptor() ([]byte, []int) { return fileDescriptor_7ac67a7adf3df9c7, []int{0} } +// StreamTeardownMode negotiates whether a completed logical pipeline stream can +// be retired without closing its exclusive TCP backend. Zero intentionally +// preserves the legacy close-the-backend behavior for rolling upgrades. +type StreamTeardownMode int32 + +const ( + StreamTeardownMode_LegacyClose StreamTeardownMode = 0 + StreamTeardownMode_FinishAck StreamTeardownMode = 1 +) + +var StreamTeardownMode_name = map[int32]string{ + 0: "LegacyClose", + 1: "FinishAck", +} + +var StreamTeardownMode_value = map[string]int32{ + "LegacyClose": 0, + "FinishAck": 1, +} + +func (x StreamTeardownMode) String() string { + return proto.EnumName(StreamTeardownMode_name, int32(x)) +} + +func (StreamTeardownMode) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_7ac67a7adf3df9c7, []int{1} +} + type Status int32 const ( @@ -91,7 +125,7 @@ func (x Status) String() string { } func (Status) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_7ac67a7adf3df9c7, []int{1} + return fileDescriptor_7ac67a7adf3df9c7, []int{2} } type SampleFunc_SampleType int32 @@ -191,19 +225,21 @@ func (Pipeline_PipelineType) EnumDescriptor() ([]byte, []int) { } type Message struct { - Sid Status `protobuf:"varint,1,opt,name=sid,proto3,enum=pipeline.Status" json:"sid,omitempty"` - Cmd Method `protobuf:"varint,2,opt,name=cmd,proto3,enum=pipeline.Method" json:"cmd,omitempty"` - Err []byte `protobuf:"bytes,3,opt,name=err,proto3" json:"err,omitempty"` - Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` - ProcInfoData []byte `protobuf:"bytes,5,opt,name=proc_info_data,json=procInfoData,proto3" json:"proc_info_data,omitempty"` - Analyse []byte `protobuf:"bytes,6,opt,name=analyse,proto3" json:"analyse,omitempty"` - Id uint64 `protobuf:"varint,7,opt,name=id,proto3" json:"id,omitempty"` - Uuid []byte `protobuf:"bytes,8,opt,name=uuid,proto3" json:"uuid,omitempty"` - NeedNotReply bool `protobuf:"varint,9,opt,name=needNotReply,proto3" json:"needNotReply,omitempty"` - DebugMsg string `protobuf:"bytes,10,opt,name=debugMsg,proto3" json:"debugMsg,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Sid Status `protobuf:"varint,1,opt,name=sid,proto3,enum=pipeline.Status" json:"sid,omitempty"` + Cmd Method `protobuf:"varint,2,opt,name=cmd,proto3,enum=pipeline.Method" json:"cmd,omitempty"` + Err []byte `protobuf:"bytes,3,opt,name=err,proto3" json:"err,omitempty"` + Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` + ProcInfoData []byte `protobuf:"bytes,5,opt,name=proc_info_data,json=procInfoData,proto3" json:"proc_info_data,omitempty"` + Analyse []byte `protobuf:"bytes,6,opt,name=analyse,proto3" json:"analyse,omitempty"` + Id uint64 `protobuf:"varint,7,opt,name=id,proto3" json:"id,omitempty"` + Uuid []byte `protobuf:"bytes,8,opt,name=uuid,proto3" json:"uuid,omitempty"` + NeedNotReply bool `protobuf:"varint,9,opt,name=needNotReply,proto3" json:"needNotReply,omitempty"` + DebugMsg string `protobuf:"bytes,10,opt,name=debugMsg,proto3" json:"debugMsg,omitempty"` + RequestedTeardownMode StreamTeardownMode `protobuf:"varint,11,opt,name=requested_teardown_mode,json=requestedTeardownMode,proto3,enum=pipeline.StreamTeardownMode" json:"requested_teardown_mode,omitempty"` + AcceptedTeardownMode StreamTeardownMode `protobuf:"varint,12,opt,name=accepted_teardown_mode,json=acceptedTeardownMode,proto3,enum=pipeline.StreamTeardownMode" json:"accepted_teardown_mode,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Message) Reset() { *m = Message{} } @@ -309,6 +345,20 @@ func (m *Message) GetDebugMsg() string { return "" } +func (m *Message) GetRequestedTeardownMode() StreamTeardownMode { + if m != nil { + return m.RequestedTeardownMode + } + return StreamTeardownMode_LegacyClose +} + +func (m *Message) GetAcceptedTeardownMode() StreamTeardownMode { + if m != nil { + return m.AcceptedTeardownMode + } + return StreamTeardownMode_LegacyClose +} + type Connector struct { PipelineId int32 `protobuf:"varint,1,opt,name=pipeline_id,json=pipelineId,proto3" json:"pipeline_id,omitempty"` ConnectorIndex int32 `protobuf:"varint,2,opt,name=connector_index,json=connectorIndex,proto3" json:"connector_index,omitempty"` @@ -5276,6 +5326,7 @@ func (m *Apply) GetTypes() []plan.Type { func init() { proto.RegisterEnum("pipeline.Method", Method_name, Method_value) + proto.RegisterEnum("pipeline.StreamTeardownMode", StreamTeardownMode_name, StreamTeardownMode_value) proto.RegisterEnum("pipeline.Status", Status_name, Status_value) proto.RegisterEnum("pipeline.SampleFunc_SampleType", SampleFunc_SampleType_name, SampleFunc_SampleType_value) proto.RegisterEnum("pipeline.SessionLoggerInfo_LogLevel", SessionLoggerInfo_LogLevel_name, SessionLoggerInfo_LogLevel_value) @@ -5340,389 +5391,396 @@ func init() { func init() { proto.RegisterFile("pipeline.proto", fileDescriptor_7ac67a7adf3df9c7) } var fileDescriptor_7ac67a7adf3df9c7 = []byte{ - // 6106 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x3b, 0x4d, 0x6f, 0x1c, 0x47, - 0x76, 0x9e, 0xef, 0x9e, 0x37, 0x1f, 0x1c, 0x96, 0xbe, 0xc6, 0xf2, 0x17, 0xdd, 0x2b, 0xc9, 0x5c, - 0xd9, 0xa6, 0x6c, 0x7a, 0xb5, 0xeb, 0xfd, 0x5e, 0x8a, 0x92, 0xd6, 0xdc, 0x25, 0x29, 0xa6, 0x49, - 0xc5, 0x80, 0x0f, 0x69, 0x34, 0xbb, 0x6b, 0x86, 0x6d, 0xf6, 0x74, 0xb5, 0xba, 0xab, 0x25, 0x52, - 0xa7, 0x04, 0xc9, 0x29, 0x7f, 0x20, 0x40, 0x72, 0x59, 0x04, 0x01, 0x82, 0x4d, 0x10, 0x6c, 0x80, - 0x20, 0xb9, 0xe4, 0x0f, 0xec, 0x31, 0xa7, 0x1c, 0x72, 0x08, 0x82, 0xcd, 0x31, 0x40, 0x6e, 0x01, - 0xf6, 0x12, 0x20, 0x78, 0xaf, 0xaa, 0xba, 0x7b, 0x86, 0xa4, 0x6c, 0x39, 0xc1, 0x5e, 0xb2, 0xa7, - 0xa9, 0x7a, 0xef, 0x55, 0x75, 0xcd, 0xab, 0xf7, 0x5e, 0xbd, 0x8f, 0x2a, 0x18, 0x26, 0x61, 0xc2, - 0xa3, 0x30, 0xe6, 0x6b, 0x49, 0x2a, 0xa4, 0x60, 0x96, 0xe9, 0x5f, 0x7f, 0x7f, 0x1a, 0xca, 0xa3, - 0xfc, 0x70, 0xcd, 0x17, 0xb3, 0x3b, 0x53, 0x31, 0x15, 0x77, 0x88, 0xe0, 0x30, 0x9f, 0x50, 0x8f, - 0x3a, 0xd4, 0x52, 0x03, 0xaf, 0x43, 0x24, 0xfc, 0x63, 0xd3, 0x4e, 0x22, 0x2f, 0xd6, 0xed, 0x25, - 0x19, 0xce, 0x78, 0x26, 0xbd, 0x59, 0xa2, 0x01, 0x5d, 0x79, 0xa2, 0x71, 0xf6, 0x9f, 0xd5, 0xa1, - 0xb3, 0xc3, 0xb3, 0xcc, 0x9b, 0x72, 0x66, 0x43, 0x23, 0x0b, 0x83, 0x71, 0x6d, 0xa5, 0xb6, 0x3a, - 0x5c, 0x1f, 0xad, 0x15, 0xcb, 0xda, 0x97, 0x9e, 0xcc, 0x33, 0x07, 0x91, 0x48, 0xe3, 0xcf, 0x82, - 0x71, 0x7d, 0x91, 0x66, 0x87, 0xcb, 0x23, 0x11, 0x38, 0x88, 0x64, 0x23, 0x68, 0xf0, 0x34, 0x1d, - 0x37, 0x56, 0x6a, 0xab, 0x7d, 0x07, 0x9b, 0x8c, 0x41, 0x33, 0xf0, 0xa4, 0x37, 0x6e, 0x12, 0x88, - 0xda, 0xec, 0x06, 0x0c, 0x93, 0x54, 0xf8, 0x6e, 0x18, 0x4f, 0x84, 0x4b, 0xd8, 0x16, 0x61, 0xfb, - 0x08, 0xdd, 0x8a, 0x27, 0xe2, 0x3e, 0x52, 0x8d, 0xa1, 0xe3, 0xc5, 0x5e, 0x74, 0x9a, 0xf1, 0x71, - 0x9b, 0xd0, 0xa6, 0xcb, 0x86, 0x50, 0x0f, 0x83, 0x71, 0x67, 0xa5, 0xb6, 0xda, 0x74, 0xea, 0x61, - 0x80, 0xdf, 0xc8, 0xf3, 0x30, 0x18, 0x5b, 0xea, 0x1b, 0xd8, 0x66, 0x36, 0xf4, 0x63, 0xce, 0x83, - 0x5d, 0x21, 0x1d, 0x9e, 0x44, 0xa7, 0xe3, 0xee, 0x4a, 0x6d, 0xd5, 0x72, 0xe6, 0x60, 0xec, 0x3a, - 0x58, 0x01, 0x3f, 0xcc, 0xa7, 0x3b, 0xd9, 0x74, 0x0c, 0x2b, 0xb5, 0xd5, 0xae, 0x53, 0xf4, 0xed, - 0xc7, 0xd0, 0xdd, 0x14, 0x71, 0xcc, 0x7d, 0x29, 0x52, 0xf6, 0x16, 0xf4, 0xcc, 0xdf, 0x75, 0x35, - 0x9b, 0x5a, 0x0e, 0x18, 0xd0, 0x56, 0xc0, 0xde, 0x81, 0x25, 0xdf, 0x50, 0xbb, 0x61, 0x1c, 0xf0, - 0x13, 0xe2, 0x53, 0xcb, 0x19, 0x16, 0xe0, 0x2d, 0x84, 0xda, 0x7f, 0xda, 0x80, 0xce, 0xfe, 0x51, - 0x3e, 0x99, 0x44, 0x9c, 0xdd, 0x80, 0x81, 0x6e, 0x6e, 0x8a, 0x68, 0x2b, 0x38, 0xd1, 0xf3, 0xce, - 0x03, 0xd9, 0x0a, 0xf4, 0x34, 0xe0, 0xe0, 0x34, 0xe1, 0x7a, 0xda, 0x2a, 0x68, 0x7e, 0x9e, 0x9d, - 0x30, 0x26, 0xf6, 0x37, 0x9c, 0x79, 0xe0, 0x02, 0x95, 0x77, 0x42, 0x3b, 0x32, 0x4f, 0xe5, 0xd1, - 0xd7, 0x36, 0xa2, 0xf0, 0x29, 0x77, 0xf8, 0x74, 0x33, 0x96, 0xb4, 0x2f, 0x2d, 0xa7, 0x0a, 0x62, - 0xeb, 0x70, 0x25, 0x53, 0x43, 0xdc, 0xd4, 0x8b, 0xa7, 0x3c, 0x73, 0xf3, 0x30, 0x96, 0xdf, 0xfc, - 0xc6, 0xb8, 0xbd, 0xd2, 0x58, 0x6d, 0x3a, 0x97, 0x34, 0xd2, 0x21, 0xdc, 0x63, 0x42, 0xb1, 0x0f, - 0xe0, 0xf2, 0xc2, 0x18, 0x35, 0xa4, 0xb3, 0xd2, 0x58, 0x6d, 0x38, 0x6c, 0x6e, 0xc8, 0x16, 0x8d, - 0x78, 0x00, 0xcb, 0x69, 0x1e, 0xa3, 0xf4, 0x3e, 0x0c, 0x23, 0xc9, 0xd3, 0xfd, 0x84, 0xfb, 0xb4, - 0xbf, 0xbd, 0xf5, 0x6b, 0x6b, 0x24, 0xe0, 0xce, 0x22, 0xda, 0x39, 0x3b, 0x82, 0xbd, 0x57, 0x30, - 0xef, 0xc1, 0x49, 0x92, 0x92, 0x10, 0xf4, 0xd6, 0x41, 0x4d, 0x80, 0x10, 0xa7, 0x8a, 0xb6, 0x7f, - 0x5d, 0x07, 0xeb, 0x7e, 0x98, 0x25, 0x9e, 0xf4, 0x8f, 0xd8, 0x35, 0xe8, 0x4c, 0xf2, 0xd8, 0x2f, - 0xf7, 0xbb, 0x8d, 0xdd, 0xad, 0x80, 0x7d, 0x0f, 0x96, 0x22, 0xe1, 0x7b, 0x91, 0x5b, 0x6c, 0xed, - 0xb8, 0xbe, 0xd2, 0x58, 0xed, 0xad, 0x5f, 0x2a, 0x75, 0xa2, 0x10, 0x1d, 0x67, 0x48, 0xb4, 0xa5, - 0x28, 0x7d, 0x1f, 0x46, 0x29, 0x9f, 0x09, 0xc9, 0x2b, 0xc3, 0x1b, 0x34, 0x9c, 0x95, 0xc3, 0x3f, - 0x4d, 0xbd, 0x64, 0x57, 0x04, 0xdc, 0x59, 0x52, 0xb4, 0xe5, 0xf0, 0x0f, 0x2b, 0xdc, 0xe7, 0x53, - 0x37, 0x0c, 0x4e, 0x5c, 0xfa, 0xc0, 0xb8, 0xb9, 0xd2, 0x58, 0x6d, 0x95, 0xac, 0xe4, 0xd3, 0xad, - 0xe0, 0x64, 0x1b, 0x31, 0xec, 0x23, 0xb8, 0xba, 0x38, 0x44, 0xcd, 0x3a, 0x6e, 0xd1, 0x98, 0x4b, - 0x73, 0x63, 0x1c, 0x42, 0xb1, 0xb7, 0xa1, 0x6f, 0x06, 0x49, 0x14, 0xbb, 0xb6, 0x12, 0x84, 0xac, - 0x22, 0x76, 0xd7, 0xa0, 0x13, 0x66, 0x6e, 0x16, 0xc6, 0xc7, 0xa4, 0x8a, 0x96, 0xd3, 0x0e, 0xb3, - 0xfd, 0x30, 0x3e, 0x66, 0xaf, 0x82, 0x95, 0x72, 0x5f, 0x61, 0x2c, 0xc2, 0x74, 0x52, 0xee, 0x13, - 0xea, 0x1a, 0x60, 0xd3, 0xf5, 0x25, 0xd7, 0x0a, 0xd9, 0x4e, 0xb9, 0xbf, 0x29, 0xb9, 0x9d, 0x41, - 0x6b, 0x87, 0xa7, 0x53, 0x8e, 0x3a, 0x89, 0x03, 0xf7, 0x7d, 0x2f, 0x26, 0xbe, 0x5b, 0x4e, 0xd1, - 0x47, 0x8b, 0x90, 0x78, 0xa9, 0x0c, 0xbd, 0x88, 0xd4, 0xc0, 0x72, 0x4c, 0x97, 0xbd, 0x06, 0xdd, - 0x4c, 0x7a, 0xa9, 0xc4, 0x7f, 0x47, 0xe2, 0xdf, 0x72, 0x2c, 0x02, 0xa0, 0x06, 0x5d, 0x83, 0x0e, - 0x8f, 0x03, 0x42, 0x35, 0xd5, 0x4e, 0xf2, 0x38, 0xd8, 0x0a, 0x4e, 0xec, 0xbf, 0xab, 0xc1, 0x60, - 0x27, 0x8f, 0x64, 0xb8, 0x91, 0x4e, 0x73, 0x3e, 0x8b, 0x25, 0x5a, 0x92, 0xfb, 0x61, 0x26, 0xf5, - 0x97, 0xa9, 0xcd, 0x56, 0xa1, 0xfb, 0xe3, 0x54, 0xe4, 0x09, 0x49, 0x90, 0xda, 0xe9, 0xaa, 0x04, - 0x95, 0x48, 0x94, 0xb6, 0x47, 0x69, 0xc0, 0xd3, 0x7b, 0xa7, 0x44, 0xdb, 0x38, 0x43, 0x5b, 0x45, - 0xb3, 0xd7, 0xa1, 0xbb, 0xcf, 0x13, 0x2f, 0xf5, 0x50, 0x04, 0x9a, 0x64, 0x7e, 0x4a, 0x00, 0xfe, - 0x57, 0x22, 0xde, 0x0a, 0xb4, 0x12, 0x9a, 0xae, 0x3d, 0x85, 0xee, 0xc6, 0x74, 0x9a, 0xf2, 0xa9, - 0x27, 0xc9, 0x14, 0x8a, 0x84, 0x96, 0xdb, 0x70, 0xea, 0x22, 0x21, 0x73, 0x8b, 0x7f, 0x40, 0xf1, - 0x87, 0xda, 0xec, 0x4d, 0x68, 0xf2, 0xf3, 0xd7, 0x43, 0x70, 0x76, 0x15, 0xda, 0xbe, 0x88, 0x27, - 0xe1, 0x54, 0x1b, 0x69, 0xdd, 0xb3, 0xff, 0xaa, 0x01, 0x2d, 0xfa, 0x73, 0xc8, 0x5e, 0x34, 0x9c, - 0x2e, 0x7f, 0xea, 0x45, 0x66, 0x57, 0x10, 0xf0, 0xe0, 0xa9, 0x17, 0xb1, 0x15, 0x68, 0xe1, 0x34, - 0xd9, 0x39, 0xbc, 0x51, 0x08, 0x76, 0x0b, 0x5a, 0x28, 0x44, 0xd9, 0xfc, 0x0a, 0x50, 0x88, 0xee, - 0x35, 0x7f, 0xf9, 0xaf, 0x6f, 0xbd, 0xe2, 0x28, 0x34, 0x7b, 0x07, 0x9a, 0xde, 0x74, 0x9a, 0x91, - 0x2c, 0xcf, 0xa9, 0x53, 0xf1, 0x7f, 0x1d, 0x22, 0x60, 0x77, 0xa1, 0xab, 0xf6, 0x0d, 0xa9, 0x5b, - 0x44, 0x7d, 0xad, 0x72, 0x20, 0x55, 0xb7, 0xd4, 0x29, 0x29, 0x91, 0xe3, 0x61, 0xa6, 0x15, 0x9e, - 0x24, 0xda, 0x72, 0x4a, 0x00, 0x9e, 0x18, 0x49, 0xca, 0x37, 0xa2, 0x48, 0xf8, 0xfb, 0xe1, 0x73, - 0xae, 0xcf, 0x97, 0x39, 0x18, 0xbb, 0x05, 0xc3, 0x3d, 0x25, 0x72, 0x0e, 0xcf, 0xf2, 0x48, 0x66, - 0xfa, 0xcc, 0x59, 0x80, 0xb2, 0x35, 0x60, 0x73, 0x90, 0x03, 0xfa, 0xfb, 0xdd, 0x95, 0xc6, 0xea, - 0xc0, 0x39, 0x07, 0xc3, 0xbe, 0x06, 0x83, 0x29, 0x72, 0x3a, 0x8c, 0xa7, 0xee, 0x24, 0xf2, 0xf0, - 0x38, 0x6a, 0xe0, 0x71, 0x65, 0x80, 0x0f, 0x23, 0x6f, 0x4a, 0x42, 0x9e, 0x84, 0x51, 0xe4, 0xce, - 0xf8, 0x6c, 0xdc, 0xa3, 0x2d, 0xb7, 0x08, 0xb0, 0xc3, 0x67, 0xf6, 0x5f, 0x37, 0xa1, 0xbd, 0x15, - 0x67, 0x3c, 0x95, 0xa8, 0x42, 0xde, 0x64, 0xc2, 0x7d, 0xc9, 0x95, 0xe9, 0x6a, 0x3a, 0x45, 0x1f, - 0x59, 0x70, 0x20, 0x3e, 0x4d, 0x43, 0xc9, 0xf7, 0x3f, 0xd2, 0x42, 0x52, 0x02, 0xd8, 0x6d, 0x58, - 0xf6, 0x82, 0xc0, 0x35, 0xd4, 0x6e, 0x2a, 0x9e, 0x65, 0xa4, 0x4e, 0x96, 0xb3, 0xe4, 0x05, 0xc1, - 0x86, 0x86, 0x3b, 0xe2, 0x59, 0xc6, 0xde, 0x86, 0x46, 0xca, 0x27, 0x24, 0x32, 0xbd, 0xf5, 0x25, - 0xb5, 0xa5, 0x8f, 0x0e, 0x3f, 0xe7, 0xbe, 0x74, 0xf8, 0xc4, 0x41, 0x1c, 0xbb, 0x0c, 0x2d, 0x4f, - 0xca, 0x54, 0x6d, 0x51, 0xd7, 0x51, 0x1d, 0xb6, 0x06, 0x97, 0x48, 0x6d, 0x65, 0x28, 0x62, 0x57, - 0x7a, 0x87, 0x11, 0x9e, 0xa9, 0x99, 0x3e, 0x3e, 0x96, 0x0b, 0xd4, 0x01, 0x62, 0xb6, 0x82, 0x0c, - 0x0f, 0x9c, 0x45, 0xfa, 0xd8, 0x9b, 0xf1, 0x8c, 0x4e, 0x8f, 0xae, 0x73, 0x69, 0x7e, 0xc4, 0x2e, - 0xa2, 0x90, 0x9f, 0xe5, 0x18, 0x54, 0x7c, 0x8b, 0x74, 0xa8, 0x5f, 0x00, 0xd1, 0x2e, 0x5c, 0x81, - 0x76, 0x98, 0xb9, 0x3c, 0x0e, 0xb4, 0x2d, 0x6a, 0x85, 0xd9, 0x83, 0x38, 0x60, 0xef, 0x42, 0x57, - 0x7d, 0x25, 0xe0, 0x13, 0x72, 0x0b, 0x7a, 0xeb, 0x43, 0x2d, 0xb1, 0x08, 0xbe, 0xcf, 0x27, 0x8e, - 0x25, 0x75, 0x0b, 0x3d, 0x03, 0x29, 0x5c, 0x7e, 0x22, 0x79, 0x1a, 0x7b, 0x11, 0xed, 0x8a, 0xe5, - 0x80, 0x14, 0x0f, 0x34, 0x84, 0xdd, 0x85, 0x6b, 0x06, 0xeb, 0x66, 0x72, 0x26, 0xdd, 0x3c, 0x0e, - 0x4f, 0xdc, 0xd8, 0x8b, 0xc5, 0xb8, 0x4f, 0x5b, 0x78, 0xd9, 0xa0, 0xf7, 0xe5, 0x4c, 0x3e, 0x8e, - 0xc3, 0x93, 0x5d, 0x2f, 0x16, 0x6c, 0x15, 0x46, 0xc5, 0x30, 0xf9, 0x9c, 0xfe, 0xf0, 0x78, 0x40, - 0x36, 0x62, 0x68, 0xe0, 0x07, 0xcf, 0xf1, 0xbf, 0xa2, 0x79, 0xaf, 0x52, 0x8a, 0xc9, 0x24, 0xe3, - 0xd2, 0xcd, 0xb8, 0x3f, 0x1e, 0xd2, 0x7f, 0xbe, 0x54, 0xd2, 0x3f, 0x22, 0xdc, 0x3e, 0xf7, 0xed, - 0x3f, 0xac, 0x41, 0x8f, 0xf4, 0xe2, 0x71, 0x12, 0xa0, 0x19, 0xf9, 0x1a, 0x0c, 0xe6, 0x37, 0x5d, - 0xc9, 0x4d, 0xdf, 0xab, 0xee, 0xf8, 0x55, 0x68, 0x6f, 0xf8, 0xc8, 0x3c, 0x12, 0x9c, 0x81, 0xa3, - 0x7b, 0xec, 0x5b, 0xb0, 0x94, 0xd3, 0x34, 0xae, 0x2f, 0x4f, 0xdc, 0x08, 0xcd, 0x8f, 0x52, 0x74, - 0x2d, 0x15, 0xea, 0x1b, 0x9b, 0xf2, 0xc4, 0x19, 0xe4, 0xa6, 0xb9, 0x1d, 0x66, 0xd2, 0x7e, 0x03, - 0x5a, 0x1b, 0x69, 0xea, 0x9d, 0x92, 0xa0, 0x60, 0x63, 0x5c, 0xa3, 0x13, 0x49, 0x75, 0x6c, 0x1f, - 0x1a, 0x3b, 0x5e, 0xc2, 0x6e, 0x42, 0x7d, 0x96, 0x10, 0xa6, 0xb7, 0x7e, 0xa5, 0xa2, 0xe5, 0x5e, - 0xb2, 0xb6, 0x93, 0x3c, 0x88, 0x65, 0x7a, 0xea, 0xd4, 0x67, 0xc9, 0xf5, 0xbb, 0xd0, 0xd1, 0x5d, - 0xf4, 0x42, 0x8f, 0xf9, 0x29, 0xfd, 0x87, 0xae, 0x83, 0x4d, 0xfc, 0xc0, 0x53, 0x2f, 0xca, 0x8d, - 0xfb, 0xa4, 0x3a, 0xdf, 0xa9, 0x7f, 0x5c, 0xb3, 0xff, 0xab, 0x09, 0xd6, 0x7d, 0x1e, 0x71, 0xfa, - 0x27, 0x36, 0xf4, 0xab, 0x32, 0x6e, 0xb8, 0x30, 0x27, 0xf7, 0x36, 0xf4, 0xd5, 0x19, 0x49, 0xa3, - 0xb8, 0x56, 0xa2, 0x39, 0x18, 0x1a, 0xef, 0xad, 0x7b, 0xb9, 0x7f, 0xcc, 0x25, 0x69, 0xcf, 0xc0, - 0x31, 0x5d, 0xc4, 0xec, 0x6a, 0x4c, 0x53, 0x61, 0x74, 0x97, 0xbd, 0x0e, 0x90, 0x8a, 0x67, 0x6e, - 0xa8, 0x0e, 0x2a, 0x65, 0xf3, 0xad, 0x54, 0x3c, 0xdb, 0xc2, 0xa3, 0xea, 0x37, 0xa2, 0x34, 0xdf, - 0x82, 0x71, 0x45, 0x69, 0xd0, 0x5d, 0x75, 0xc3, 0xd8, 0x3d, 0x44, 0x6f, 0x48, 0xeb, 0x4f, 0x39, - 0x27, 0x79, 0xb3, 0x5b, 0xf1, 0x3d, 0x72, 0x95, 0xb4, 0x29, 0xe8, 0xbe, 0xc0, 0x14, 0x9c, 0x6b, - 0x59, 0xe0, 0x7c, 0xcb, 0x72, 0x0f, 0x60, 0x9f, 0x4f, 0x67, 0x3c, 0x96, 0x3b, 0x5e, 0x32, 0xee, - 0xd1, 0xc6, 0xdb, 0xe5, 0xc6, 0x9b, 0xdd, 0x5a, 0x2b, 0x89, 0x94, 0x14, 0x54, 0x46, 0xa1, 0xff, - 0xe2, 0x7b, 0xb1, 0x2b, 0xd3, 0x3c, 0xf6, 0x3d, 0xc9, 0x49, 0xd7, 0x2c, 0xa7, 0xe7, 0x7b, 0xf1, - 0x81, 0x06, 0x55, 0xd4, 0x7f, 0x50, 0x55, 0xff, 0x5b, 0xb0, 0x94, 0xa4, 0xe1, 0xcc, 0x4b, 0x4f, - 0xdd, 0x63, 0x7e, 0x4a, 0x9b, 0xa1, 0x14, 0x69, 0xa0, 0xc1, 0x3f, 0xe5, 0xa7, 0x5b, 0xc1, 0xc9, - 0xf5, 0xef, 0xc3, 0xd2, 0xc2, 0x02, 0x5e, 0x4a, 0xee, 0x7e, 0xd6, 0x80, 0xee, 0x5e, 0xca, 0xb5, - 0xc9, 0x7e, 0x0b, 0x7a, 0x99, 0x7f, 0xc4, 0x67, 0x9e, 0xd2, 0x74, 0x35, 0x03, 0x28, 0x10, 0x69, - 0xf9, 0x9c, 0x51, 0xaa, 0x7f, 0x81, 0x51, 0x1a, 0x41, 0x43, 0xf9, 0x41, 0xa8, 0x4c, 0xd8, 0x2c, - 0x2d, 0x71, 0xb3, 0x6a, 0x89, 0x57, 0xa0, 0x7f, 0xe4, 0x65, 0xae, 0x97, 0x4b, 0xe1, 0xfa, 0x22, - 0x22, 0xa1, 0xb3, 0x1c, 0x38, 0xf2, 0xb2, 0x8d, 0x5c, 0x8a, 0x4d, 0x11, 0xb1, 0x37, 0x00, 0x7c, - 0x11, 0x69, 0xa3, 0xa2, 0x9d, 0xc0, 0xae, 0x2f, 0x22, 0x65, 0x49, 0x50, 0x2a, 0x79, 0x26, 0xc3, - 0x99, 0xa7, 0xb7, 0xd4, 0xf5, 0x45, 0x1e, 0x4b, 0x3a, 0x39, 0x1b, 0xce, 0x72, 0x81, 0x72, 0xc4, - 0xb3, 0x4d, 0x44, 0xb0, 0x0f, 0x60, 0xe8, 0x8b, 0x59, 0xe2, 0x26, 0xc8, 0x59, 0xf2, 0x49, 0xac, - 0x33, 0x1e, 0x79, 0x1f, 0x29, 0xf6, 0x8e, 0xb9, 0x72, 0x92, 0xd6, 0x61, 0xc9, 0x8f, 0xf2, 0x4c, - 0xf2, 0xd4, 0x3d, 0xd4, 0x43, 0xce, 0x3a, 0xf1, 0x03, 0x4d, 0xa2, 0x1d, 0x2b, 0x1b, 0x06, 0x61, - 0xe6, 0x8a, 0x28, 0x70, 0x95, 0xb9, 0xd1, 0x72, 0xd6, 0x0b, 0xb3, 0x47, 0x51, 0xa0, 0x0d, 0x9e, - 0xa2, 0x89, 0xf9, 0x33, 0x43, 0xd3, 0x33, 0x34, 0xbb, 0xfc, 0x99, 0xa2, 0xb1, 0xff, 0xb9, 0x0e, - 0x9d, 0x3d, 0x91, 0xc9, 0xfb, 0xb3, 0xc8, 0x88, 0x78, 0xed, 0x65, 0x45, 0xbc, 0x7e, 0xbe, 0x88, - 0x9f, 0x23, 0x64, 0x8d, 0x73, 0x84, 0x0c, 0x8f, 0x81, 0x2a, 0x1d, 0x09, 0x87, 0x72, 0x15, 0x87, - 0x25, 0x21, 0x09, 0xc8, 0x6b, 0xe8, 0xdb, 0xb8, 0x81, 0xb2, 0x49, 0x6a, 0x23, 0xad, 0x30, 0xd3, - 0xf6, 0x48, 0x21, 0x43, 0x92, 0x35, 0xed, 0xf8, 0x58, 0x61, 0xa6, 0x65, 0xef, 0xdb, 0xf0, 0x6a, - 0x31, 0xd2, 0x7d, 0x16, 0xca, 0x23, 0x91, 0x4b, 0x77, 0x42, 0x31, 0x54, 0xa6, 0x3d, 0xfb, 0xab, - 0x66, 0xa6, 0x4f, 0x15, 0x5a, 0x45, 0x58, 0xe4, 0x87, 0x4d, 0xf2, 0x28, 0x72, 0x25, 0x3f, 0x91, - 0x7a, 0x2b, 0xc7, 0x8a, 0x37, 0x9a, 0x6f, 0x0f, 0xf3, 0x28, 0x3a, 0xe0, 0x27, 0x12, 0x8d, 0xbf, - 0x35, 0xd1, 0x1d, 0xfb, 0x4f, 0x9a, 0x00, 0xdb, 0xc2, 0x3f, 0x3e, 0xf0, 0xd2, 0x29, 0x97, 0x18, - 0x2f, 0x18, 0x8b, 0xa6, 0x2d, 0x6e, 0x47, 0x2a, 0x3b, 0xc6, 0xd6, 0xe1, 0xaa, 0xf9, 0xff, 0x28, - 0x87, 0x18, 0xbb, 0x28, 0x93, 0xa4, 0x15, 0x8a, 0x69, 0xac, 0x8a, 0x95, 0xc9, 0x1e, 0xb1, 0x8f, - 0x4b, 0xde, 0xe2, 0x18, 0x79, 0x9a, 0x10, 0x6f, 0xcf, 0xf3, 0x3b, 0x07, 0xe5, 0xf0, 0x83, 0xd3, - 0x84, 0x7d, 0x00, 0x57, 0x52, 0x3e, 0x49, 0x79, 0x76, 0xe4, 0xca, 0xac, 0xfa, 0x31, 0x15, 0x36, - 0x2c, 0x6b, 0xe4, 0x41, 0x56, 0x7c, 0xeb, 0x03, 0xb8, 0xa2, 0x38, 0xb5, 0xb8, 0x3c, 0x65, 0xbf, - 0x97, 0x15, 0xb2, 0xba, 0xba, 0x37, 0x80, 0x72, 0x35, 0xca, 0x26, 0x1b, 0x27, 0x34, 0x22, 0x66, - 0x1c, 0x46, 0x1c, 0xfd, 0xb3, 0xcd, 0x23, 0x8c, 0x83, 0xef, 0xf3, 0x89, 0x66, 0x7e, 0x09, 0x60, - 0x36, 0x34, 0x77, 0x44, 0xc0, 0x89, 0xd5, 0xc3, 0xf5, 0xe1, 0x1a, 0x65, 0x7d, 0x90, 0x93, 0x08, - 0x75, 0x08, 0xc7, 0xde, 0x01, 0x9a, 0x4e, 0x89, 0xdf, 0x59, 0x5d, 0xb1, 0x10, 0x49, 0x32, 0xf8, - 0x01, 0x5c, 0x29, 0x57, 0xe2, 0x7a, 0xd2, 0x95, 0x47, 0x9c, 0xcc, 0xa1, 0x52, 0x97, 0xe5, 0x62, - 0x51, 0x1b, 0xf2, 0xe0, 0x88, 0xa3, 0x69, 0x5c, 0x85, 0x8e, 0x38, 0xfc, 0xdc, 0x45, 0x45, 0xe8, - 0x9d, 0xaf, 0x08, 0x6d, 0x71, 0xf8, 0xb9, 0xc3, 0x27, 0xec, 0x9b, 0xd5, 0xa3, 0x64, 0x81, 0x35, - 0x7d, 0x62, 0xcd, 0xe5, 0x02, 0x5f, 0xe1, 0x8e, 0xfd, 0x31, 0xb4, 0xf1, 0xef, 0x3c, 0x4a, 0xd8, - 0x1a, 0x74, 0x24, 0x89, 0x47, 0xa6, 0x8f, 0xfe, 0xcb, 0xe5, 0x09, 0x50, 0xca, 0x8e, 0x63, 0x88, - 0x6c, 0x07, 0x96, 0x0a, 0x73, 0xfa, 0x38, 0x0e, 0x9f, 0xe4, 0x9c, 0xfd, 0x10, 0x96, 0x93, 0x94, - 0x6b, 0xb1, 0x77, 0xf3, 0x63, 0x74, 0x4f, 0xb4, 0x06, 0x5f, 0xd6, 0x52, 0x5a, 0x8c, 0x38, 0x46, - 0x09, 0x1d, 0x26, 0x73, 0x7d, 0xfb, 0x33, 0xb8, 0x56, 0x50, 0xec, 0x73, 0x5f, 0xc4, 0x81, 0x97, - 0x9e, 0xd2, 0xc9, 0xb7, 0x30, 0x77, 0xf6, 0x32, 0x73, 0xef, 0xd3, 0xdc, 0x7f, 0x59, 0x83, 0xde, - 0xc3, 0xfc, 0xf9, 0xf3, 0x53, 0xa5, 0x4b, 0xac, 0x0f, 0xb5, 0x5d, 0x9a, 0xa0, 0xee, 0xd4, 0x76, - 0xd1, 0xd5, 0xda, 0x3b, 0x46, 0xbd, 0x26, 0x39, 0xef, 0x3a, 0xba, 0x87, 0x91, 0xd4, 0xde, 0xf1, - 0xc1, 0x0b, 0x24, 0x5a, 0xa1, 0x31, 0x04, 0xb8, 0x97, 0x87, 0x11, 0xba, 0x0e, 0x5a, 0x78, 0x8b, - 0x3e, 0xc6, 0x26, 0x5b, 0x13, 0xb5, 0x94, 0x87, 0xa9, 0x98, 0x29, 0x66, 0x69, 0x93, 0x71, 0x0e, - 0xc6, 0xfe, 0x75, 0x13, 0xac, 0x4f, 0xbc, 0xec, 0xe8, 0x27, 0x22, 0x8c, 0xd9, 0x07, 0xd0, 0xfd, - 0x5c, 0x84, 0xb1, 0x4a, 0x0a, 0xa8, 0x74, 0xe1, 0x25, 0xb5, 0x88, 0x5d, 0x11, 0xf0, 0x35, 0xa4, - 0xc1, 0xd5, 0x38, 0xd6, 0xe7, 0xba, 0xa5, 0x2d, 0x6d, 0x1a, 0x4e, 0x8f, 0xa4, 0x8b, 0x40, 0x6d, - 0x12, 0x7b, 0x61, 0xe6, 0x20, 0x8c, 0x66, 0x7d, 0x1d, 0xf0, 0xd0, 0x39, 0x72, 0x45, 0xec, 0x26, - 0xc7, 0x3a, 0xe0, 0xb0, 0x10, 0xf2, 0x28, 0xde, 0x3b, 0x46, 0x95, 0x09, 0x33, 0x57, 0xa7, 0x1e, - 0xe8, 0xef, 0xcc, 0xc5, 0x6d, 0x37, 0x60, 0x88, 0x47, 0x7d, 0x76, 0x1c, 0x26, 0x6e, 0x92, 0x8a, - 0x43, 0xf3, 0x5f, 0xd0, 0x01, 0xd8, 0x3f, 0x0e, 0x93, 0x3d, 0x84, 0xd1, 0x09, 0xab, 0x13, 0x1a, - 0x68, 0x6d, 0xd5, 0x51, 0x06, 0x1a, 0x84, 0x6c, 0xa1, 0xac, 0x45, 0xa4, 0xdc, 0xd7, 0x0e, 0x9d, - 0x9c, 0x9d, 0x94, 0x47, 0xe8, 0xa7, 0x22, 0x0a, 0x65, 0x98, 0x50, 0x96, 0x42, 0xf9, 0x42, 0xa1, - 0xbe, 0x0e, 0x10, 0xf1, 0x89, 0x74, 0x51, 0x38, 0x54, 0x80, 0xb7, 0x90, 0x1d, 0x40, 0xec, 0x26, - 0x22, 0xd9, 0xbb, 0xd0, 0x53, 0x5c, 0x50, 0xb4, 0x70, 0x86, 0x16, 0x08, 0xad, 0x88, 0x6f, 0x43, - 0x2f, 0x16, 0xb1, 0xcb, 0x9f, 0x10, 0xb5, 0x56, 0xb7, 0xb9, 0x89, 0x63, 0x11, 0x3f, 0x78, 0x82, - 0xc4, 0xec, 0x8e, 0x5e, 0x83, 0x8a, 0xb1, 0xfb, 0x17, 0xc4, 0xd8, 0xb4, 0x12, 0x15, 0x6d, 0x7e, - 0x68, 0x56, 0xa2, 0x46, 0x0c, 0x2e, 0x18, 0xa1, 0xd6, 0xa3, 0x86, 0xac, 0x40, 0x9f, 0xf6, 0x7d, - 0xe6, 0x25, 0xae, 0xf4, 0xa6, 0xda, 0x25, 0x02, 0x84, 0xed, 0x78, 0xc9, 0x81, 0x37, 0x65, 0x0e, - 0xbc, 0xaa, 0xf3, 0x6f, 0xfa, 0xf0, 0x70, 0x0f, 0x51, 0xe2, 0x14, 0xd7, 0x96, 0x4c, 0x8c, 0x7e, - 0x7e, 0xe6, 0xee, 0xea, 0x5c, 0xe6, 0x8e, 0x24, 0x95, 0x02, 0x84, 0xbf, 0xa8, 0x83, 0xb5, 0x2d, - 0x44, 0xf2, 0x15, 0x45, 0xaf, 0xba, 0xa5, 0xf5, 0x8b, 0xb7, 0xb4, 0x31, 0xbf, 0xa5, 0x0b, 0xac, - 0x6f, 0x7e, 0x79, 0xd6, 0xb7, 0x5e, 0x9a, 0xf5, 0xed, 0xaf, 0xc0, 0xfa, 0xce, 0x22, 0xeb, 0xed, - 0x0e, 0xb4, 0xf6, 0xb9, 0x7c, 0x94, 0xd8, 0xbf, 0xb0, 0xa0, 0x7b, 0x9f, 0x07, 0xb9, 0x62, 0x58, - 0xf5, 0xef, 0xd7, 0x2e, 0xfe, 0xfb, 0xf5, 0xf9, 0xbf, 0x8f, 0xe7, 0x87, 0x91, 0xe8, 0x73, 0x52, - 0x46, 0x96, 0x11, 0x68, 0x14, 0xfd, 0x52, 0x9e, 0x75, 0xce, 0x66, 0x8e, 0x4d, 0x85, 0x38, 0xbf, - 0x58, 0x36, 0x5a, 0x5f, 0x49, 0x36, 0x16, 0xac, 0xc2, 0x99, 0x6c, 0xce, 0x17, 0x72, 0x6d, 0xd1, - 0x22, 0x58, 0x67, 0x2c, 0xc2, 0x36, 0x5c, 0x12, 0xb1, 0x1b, 0xe4, 0x49, 0x14, 0x62, 0xc0, 0xe0, - 0x7a, 0x2a, 0xf8, 0xed, 0x92, 0xe8, 0xbd, 0x5e, 0x11, 0xbd, 0x47, 0xf1, 0x7d, 0x43, 0xa4, 0x42, - 0x62, 0x67, 0x59, 0x2c, 0x82, 0xd0, 0x4c, 0x05, 0xb8, 0x35, 0x74, 0x1c, 0x92, 0x23, 0xa7, 0x4a, - 0x0e, 0x7d, 0x82, 0x6e, 0x8a, 0x88, 0x0c, 0xfc, 0xc7, 0xb0, 0x54, 0x52, 0x29, 0x19, 0xe9, 0x5d, - 0x20, 0x23, 0x03, 0x33, 0x50, 0x89, 0xc9, 0x6f, 0xc2, 0x0a, 0xbc, 0x0f, 0x97, 0x4c, 0xa4, 0xaf, - 0xcf, 0x74, 0xda, 0xc1, 0x21, 0x49, 0xd0, 0x48, 0x07, 0xf7, 0x74, 0x9c, 0xd3, 0x16, 0x7d, 0x17, - 0x2e, 0x57, 0xc8, 0xd1, 0x79, 0xaf, 0x5a, 0x83, 0xaa, 0xac, 0x2c, 0x17, 0x63, 0xb1, 0xbb, 0xad, - 0xb2, 0x96, 0xbd, 0x80, 0x47, 0xe6, 0x43, 0xe3, 0x91, 0x8a, 0x3d, 0x02, 0x1e, 0xe9, 0xba, 0xc8, - 0x0e, 0xdc, 0x40, 0x17, 0x1f, 0xf1, 0xbe, 0x97, 0xc8, 0x3c, 0xe5, 0x6e, 0x12, 0x79, 0x3e, 0x3f, - 0x12, 0x51, 0xc0, 0xd3, 0x72, 0x71, 0xcb, 0xb4, 0xb8, 0xb7, 0x44, 0x14, 0x6c, 0x8a, 0x68, 0x53, - 0x51, 0xee, 0x95, 0x84, 0x66, 0xad, 0x1b, 0xf0, 0xe6, 0x99, 0xe9, 0xf0, 0xe0, 0x28, 0x27, 0x62, - 0x34, 0xd1, 0xab, 0xf3, 0x13, 0x21, 0x89, 0x99, 0xe2, 0x43, 0xb8, 0xa2, 0xf6, 0x4e, 0x09, 0xf7, - 0x31, 0xe7, 0x89, 0x1b, 0x79, 0x99, 0x1c, 0x5f, 0x52, 0x67, 0x2b, 0x21, 0x49, 0x80, 0x7f, 0xca, - 0x79, 0xb2, 0xed, 0xa9, 0xaf, 0xaa, 0x21, 0xda, 0xfd, 0xa6, 0x31, 0x73, 0xbc, 0xbd, 0xac, 0xbe, - 0x4a, 0x54, 0xca, 0x07, 0xc7, 0xc1, 0x15, 0x26, 0x7f, 0x0f, 0x5e, 0x9b, 0x9b, 0x62, 0xe6, 0xa5, - 0xc7, 0xa5, 0x3f, 0x3a, 0xbe, 0x42, 0x7c, 0xbb, 0x56, 0x19, 0xbf, 0x43, 0x04, 0x6a, 0x06, 0xfb, - 0x3f, 0x5b, 0x30, 0xa4, 0x73, 0xf8, 0xb7, 0x66, 0xe3, 0xb7, 0x66, 0xe3, 0xff, 0x81, 0xd9, 0xb0, - 0x7f, 0xbf, 0x06, 0x9d, 0xbd, 0x54, 0x04, 0xb9, 0x2f, 0xbf, 0xa2, 0xa4, 0xcf, 0x4b, 0x50, 0xe3, - 0x8b, 0x24, 0xa8, 0x79, 0xe6, 0xb8, 0xfe, 0x79, 0x0d, 0xba, 0x7a, 0x09, 0xdb, 0xeb, 0x5f, 0x71, - 0x11, 0x65, 0x4d, 0xa7, 0x76, 0x6e, 0x4d, 0xe7, 0x0b, 0x57, 0x81, 0x82, 0xf5, 0x54, 0xd5, 0xab, - 0x45, 0xa2, 0x7c, 0xaa, 0x96, 0x12, 0x2c, 0x05, 0x7d, 0x94, 0xe0, 0xde, 0xd9, 0xcf, 0xa0, 0x4b, - 0x01, 0x0f, 0x59, 0x86, 0xab, 0xd0, 0x4e, 0xa9, 0x68, 0xa1, 0x17, 0xaa, 0x7b, 0x2f, 0xd6, 0xd3, - 0xfa, 0x57, 0x73, 0xfd, 0xfe, 0xb6, 0x06, 0x03, 0x8a, 0x3e, 0x1f, 0xe6, 0xb1, 0xd2, 0x84, 0x22, - 0x87, 0x55, 0x9b, 0xcf, 0x61, 0x35, 0x53, 0x0c, 0x12, 0xd5, 0x67, 0xfa, 0xea, 0x33, 0x9b, 0x22, - 0xba, 0xcf, 0x27, 0x0e, 0x61, 0x90, 0x55, 0x5e, 0x3a, 0xcd, 0xce, 0x2b, 0x7f, 0x21, 0x1c, 0xff, - 0x55, 0xe2, 0xa5, 0xde, 0x2c, 0x33, 0xe5, 0x2f, 0xd5, 0x63, 0x0c, 0x9a, 0xa4, 0x6f, 0x8a, 0x2d, - 0xd4, 0xd6, 0x89, 0x94, 0x2c, 0x8c, 0xa7, 0x85, 0xf1, 0xb0, 0xa8, 0xea, 0x39, 0x8d, 0xb8, 0xbd, - 0x01, 0x57, 0x4c, 0xda, 0x1f, 0x95, 0x72, 0x1d, 0x25, 0x8e, 0x82, 0x45, 0x33, 0x53, 0xad, 0x32, - 0xd3, 0x65, 0x68, 0x55, 0xef, 0x09, 0xa8, 0x8e, 0x7d, 0x13, 0x7a, 0x93, 0x30, 0xe2, 0x3a, 0xe1, - 0x86, 0x4b, 0xd3, 0xa9, 0xb7, 0x1a, 0x55, 0xca, 0x75, 0xcf, 0xfe, 0xfb, 0x1a, 0x5c, 0x4b, 0xbc, - 0xf4, 0x49, 0xce, 0x25, 0xa5, 0xdd, 0xa8, 0x4c, 0xe4, 0x66, 0x47, 0x5e, 0x1a, 0xa0, 0x78, 0xd2, - 0x14, 0x6a, 0x76, 0x55, 0xba, 0xee, 0x22, 0x44, 0xad, 0xe5, 0x16, 0x2c, 0x55, 0x46, 0x48, 0x2f, - 0x35, 0xa9, 0x94, 0x41, 0x2a, 0x9e, 0x51, 0xb5, 0x6f, 0x1f, 0x81, 0x18, 0xb6, 0x95, 0x74, 0x9c, - 0x6c, 0x3a, 0x55, 0x80, 0x0d, 0xd5, 0x83, 0x38, 0x40, 0xf9, 0x8c, 0xf3, 0x99, 0xca, 0x34, 0xa8, - 0xdb, 0x04, 0x9d, 0x38, 0x9f, 0x51, 0x72, 0xe1, 0x32, 0xb4, 0x0e, 0x4f, 0x25, 0xf9, 0xc4, 0x08, - 0x57, 0x1d, 0xfb, 0x5f, 0x9a, 0xd0, 0x37, 0x2c, 0xa2, 0x8a, 0xee, 0x7b, 0xd5, 0x3d, 0xed, 0xad, - 0x8f, 0xcc, 0xe6, 0x20, 0xc9, 0x86, 0x94, 0xa9, 0x89, 0x6a, 0xd5, 0x5e, 0xbf, 0x06, 0xf4, 0x47, - 0xdc, 0x2c, 0x7c, 0xce, 0x69, 0xc3, 0x1b, 0x8e, 0x85, 0x00, 0x2a, 0xcd, 0x6d, 0xc0, 0x72, 0x85, - 0x75, 0xae, 0x14, 0xd2, 0x8b, 0xf4, 0x9e, 0x57, 0xaa, 0x06, 0x15, 0x12, 0x67, 0x09, 0x3b, 0x2a, - 0x93, 0x79, 0x80, 0xd4, 0x28, 0x4b, 0xbe, 0x88, 0x4c, 0xfd, 0x71, 0x41, 0x96, 0x10, 0x43, 0xf9, - 0xd0, 0x94, 0xa3, 0x69, 0xca, 0x9e, 0x44, 0x5a, 0x32, 0xba, 0x0a, 0xb2, 0xff, 0x24, 0x2a, 0x16, - 0x48, 0x82, 0xdf, 0x26, 0x31, 0xa5, 0x05, 0x92, 0xca, 0xbe, 0x0f, 0x3d, 0x91, 0x86, 0xd3, 0x90, - 0x12, 0x22, 0x2a, 0x11, 0xbf, 0xf8, 0x11, 0x50, 0x04, 0x9b, 0xf8, 0x29, 0x1b, 0xda, 0x4a, 0x99, - 0xce, 0xc9, 0x91, 0x6a, 0x0c, 0x6e, 0x66, 0x26, 0xd3, 0xd0, 0x97, 0xb8, 0x1c, 0x77, 0x26, 0x02, - 0x53, 0x56, 0x1f, 0x28, 0xf0, 0xfe, 0x93, 0x88, 0x72, 0x42, 0xb7, 0x60, 0xc9, 0x17, 0x51, 0x3e, - 0x8b, 0x69, 0x65, 0x6e, 0xc4, 0x63, 0x3a, 0x45, 0x5a, 0xce, 0x40, 0x81, 0x71, 0x7d, 0xdb, 0x3c, - 0xd6, 0x65, 0x33, 0x2f, 0x8a, 0xd0, 0x20, 0x09, 0x2f, 0xd0, 0x59, 0xd1, 0xbe, 0x01, 0x6e, 0x0b, - 0x2f, 0x60, 0xdf, 0x81, 0xeb, 0x88, 0x73, 0xf9, 0x2c, 0x91, 0xa7, 0x6e, 0x9c, 0xcf, 0x78, 0x1a, - 0xfa, 0xae, 0x97, 0xb9, 0xcf, 0x79, 0x2a, 0x74, 0xa2, 0xfd, 0x2a, 0x52, 0x3c, 0x40, 0x82, 0x5d, - 0x85, 0xdf, 0xc8, 0x3e, 0xe3, 0xa9, 0x60, 0x9f, 0x51, 0x5e, 0xe8, 0x3c, 0xb9, 0x35, 0x27, 0xc9, - 0xdb, 0xe5, 0x5e, 0x5d, 0x40, 0x49, 0x55, 0x08, 0x44, 0x38, 0x46, 0x60, 0x69, 0xbc, 0xed, 0x03, - 0xec, 0xcb, 0x94, 0x7b, 0x33, 0x92, 0xac, 0x77, 0xa0, 0x23, 0x0f, 0x23, 0x4a, 0x97, 0xd7, 0xce, - 0x4d, 0x97, 0xb7, 0xe5, 0x21, 0xf2, 0xbc, 0xa2, 0x63, 0x75, 0x12, 0x55, 0xdd, 0x43, 0x09, 0x8e, - 0xc2, 0x59, 0x28, 0xf5, 0x6d, 0x1a, 0xd5, 0xb1, 0x0f, 0xa1, 0x4b, 0x33, 0xd0, 0x37, 0x8a, 0xba, - 0x76, 0xed, 0xc5, 0x75, 0xed, 0xf7, 0xa1, 0xaf, 0xed, 0xe2, 0x45, 0x85, 0xf2, 0x9e, 0xc2, 0x63, - 0x3b, 0xb3, 0xdf, 0x83, 0xee, 0xef, 0x7a, 0x51, 0xae, 0xbe, 0xf1, 0x16, 0xf4, 0xa8, 0x02, 0xe3, - 0x1e, 0x46, 0xc2, 0x3f, 0x36, 0x95, 0x01, 0x02, 0xdd, 0x43, 0x88, 0x0d, 0x60, 0x3d, 0x8e, 0x43, - 0x11, 0x6f, 0x44, 0x91, 0xfd, 0x8f, 0x6d, 0xe8, 0x7e, 0xe2, 0x65, 0x47, 0x64, 0x46, 0x51, 0x85, - 0xa9, 0x6a, 0x4f, 0xa9, 0x95, 0x99, 0x97, 0xe8, 0xca, 0x7d, 0x0f, 0x81, 0x48, 0xb5, 0xe3, 0x25, - 0x0b, 0x99, 0x97, 0xfa, 0x42, 0xe6, 0xe5, 0x6d, 0x75, 0x89, 0x4a, 0xd5, 0x80, 0xb8, 0x29, 0x05, - 0xd3, 0x04, 0xf7, 0x14, 0x88, 0xbd, 0x07, 0x8c, 0x48, 0xbc, 0x28, 0x12, 0xe4, 0xee, 0x64, 0x3c, - 0xca, 0x74, 0x92, 0x66, 0x84, 0x98, 0x0d, 0x8d, 0xd8, 0xe7, 0x4a, 0x7f, 0x2a, 0x67, 0x67, 0x6b, - 0xf1, 0xec, 0xbc, 0x0d, 0x80, 0x5e, 0x21, 0xa5, 0x05, 0x17, 0x82, 0x63, 0x95, 0x21, 0x29, 0xb1, - 0x5f, 0xc2, 0x53, 0x7b, 0x07, 0x46, 0x05, 0x45, 0xca, 0x27, 0xae, 0x1f, 0x4b, 0xed, 0xae, 0x0d, - 0x34, 0x95, 0xc3, 0x27, 0x9b, 0xb1, 0x5c, 0x74, 0xe9, 0xba, 0x67, 0x5c, 0xba, 0x1f, 0xc3, 0xa5, - 0x85, 0x03, 0x2e, 0x4b, 0xb8, 0xaf, 0x8b, 0xc3, 0x2f, 0x73, 0x1f, 0xe9, 0x55, 0xb0, 0x28, 0xd7, - 0x1e, 0xe4, 0x89, 0xd6, 0xad, 0x4e, 0x98, 0x91, 0xeb, 0x7d, 0x91, 0xdb, 0xd8, 0xff, 0xbf, 0x72, - 0x1b, 0x07, 0x5f, 0xce, 0x6d, 0x1c, 0x7e, 0x39, 0xb7, 0x71, 0xc1, 0xcd, 0x5a, 0x5a, 0x8c, 0xce, - 0x2e, 0x8c, 0x85, 0x46, 0x17, 0xc6, 0x42, 0x5f, 0x10, 0xc8, 0x2c, 0xbf, 0x30, 0x90, 0xf9, 0x12, - 0x91, 0x14, 0xfb, 0x82, 0x48, 0xca, 0x7e, 0x0c, 0x40, 0x67, 0x24, 0x2d, 0xf9, 0xa2, 0x3d, 0xaf, - 0xbd, 0xec, 0x9e, 0xdb, 0xff, 0x5d, 0x03, 0xd8, 0xf7, 0x66, 0x89, 0x72, 0x65, 0xd8, 0x8f, 0xa0, - 0x97, 0x51, 0xaf, 0x9a, 0xc8, 0x7a, 0xab, 0x72, 0xe5, 0xb2, 0x20, 0xd5, 0x4d, 0x4a, 0x6a, 0x41, - 0x56, 0xb4, 0x49, 0x5c, 0xd5, 0x0c, 0x45, 0x89, 0xa9, 0x65, 0x08, 0xe8, 0xf0, 0xbd, 0x09, 0x43, - 0x4d, 0x90, 0xf0, 0xd4, 0xe7, 0xb1, 0xb2, 0x61, 0x35, 0x67, 0xa0, 0xa0, 0x7b, 0x0a, 0xc8, 0x3e, - 0x2c, 0xc8, 0xd4, 0x29, 0x90, 0x9d, 0x13, 0x8d, 0xe9, 0x21, 0x9b, 0x8a, 0xc0, 0x5e, 0x37, 0x7f, - 0x85, 0x16, 0x62, 0x41, 0x13, 0xbf, 0x37, 0x7a, 0x85, 0xf5, 0xa0, 0xa3, 0x67, 0x1d, 0xd5, 0xd8, - 0x00, 0xba, 0x74, 0x97, 0x8b, 0x70, 0x75, 0xfb, 0x0f, 0x96, 0xa1, 0xb7, 0x15, 0x67, 0x32, 0xcd, - 0x95, 0x68, 0x96, 0x57, 0x96, 0x5a, 0x74, 0x65, 0x49, 0x57, 0x2b, 0xd5, 0xdf, 0xa0, 0x6a, 0xe5, - 0xfb, 0xd0, 0xd1, 0x97, 0xe3, 0xb4, 0x7f, 0x7b, 0xee, 0xcd, 0x3a, 0x43, 0xc3, 0xd6, 0xc0, 0x0a, - 0xf4, 0xad, 0x3d, 0x9d, 0xad, 0xab, 0x5c, 0xa5, 0x33, 0xf7, 0xf9, 0x9c, 0x82, 0x86, 0xbd, 0x0d, - 0x0d, 0x6f, 0x3a, 0x25, 0xeb, 0x43, 0x25, 0x0c, 0x43, 0x4a, 0x87, 0x89, 0x83, 0x38, 0x76, 0x07, - 0xba, 0x64, 0x16, 0x29, 0x61, 0xdd, 0x5e, 0x9c, 0xd3, 0x64, 0xc3, 0x95, 0xa5, 0x24, 0xd7, 0xf8, - 0x0e, 0x74, 0x23, 0x21, 0x12, 0x35, 0xa0, 0xb3, 0x38, 0xc0, 0xe4, 0x30, 0x1d, 0x2b, 0x32, 0xd9, - 0xcc, 0x5b, 0xd0, 0x46, 0x37, 0x45, 0x24, 0xfa, 0x78, 0xaf, 0xac, 0x83, 0x72, 0x79, 0x4e, 0x2b, - 0xc3, 0x1f, 0xb6, 0x0e, 0xa0, 0xe4, 0x9a, 0x66, 0xee, 0x2e, 0xb2, 0xa3, 0x08, 0xdb, 0x51, 0xf9, - 0x4c, 0x04, 0x7f, 0x0f, 0x46, 0x2a, 0x44, 0xab, 0x8c, 0x04, 0x53, 0x9d, 0x33, 0x23, 0xe7, 0xa3, - 0x7e, 0x67, 0x98, 0xce, 0x67, 0x01, 0xde, 0x85, 0x4e, 0xa2, 0x62, 0x14, 0xb2, 0x1c, 0xbd, 0xf5, - 0xe5, 0x72, 0xa8, 0x0e, 0x5e, 0x1c, 0x43, 0xc1, 0x7e, 0x00, 0x43, 0x55, 0x45, 0x9a, 0x68, 0x67, - 0x9d, 0xf2, 0xc3, 0x73, 0x97, 0xb2, 0xe6, 0x7c, 0x79, 0x67, 0x20, 0xe7, 0x5c, 0xfb, 0xef, 0xc2, - 0xa0, 0xbc, 0x24, 0xe3, 0x7b, 0x31, 0xd9, 0x93, 0xde, 0xfa, 0xd5, 0x72, 0x78, 0xd5, 0x6b, 0x74, - 0xfa, 0xbc, 0xea, 0x43, 0xae, 0x42, 0x5b, 0x57, 0x36, 0x47, 0x34, 0xaa, 0x72, 0x35, 0x59, 0xd5, - 0x32, 0x1c, 0x8d, 0x47, 0x5e, 0x96, 0x45, 0x9b, 0x31, 0x5b, 0xe4, 0x65, 0x51, 0xb1, 0x71, 0xba, - 0x45, 0xb1, 0x86, 0x3d, 0x98, 0x2f, 0x22, 0xa9, 0x62, 0xc9, 0x25, 0x1a, 0xfa, 0xea, 0x39, 0x43, - 0x55, 0xcd, 0xc4, 0x59, 0x4a, 0x16, 0x6a, 0x51, 0xef, 0x81, 0x25, 0xd2, 0x80, 0xaa, 0xd8, 0x94, - 0xd2, 0x21, 0x7e, 0x52, 0xed, 0x4c, 0xdd, 0x08, 0x24, 0xe3, 0xd1, 0x11, 0xaa, 0x83, 0x0e, 0x43, - 0x92, 0x8a, 0xcf, 0xb9, 0x2f, 0x95, 0xe9, 0xba, 0x72, 0xd6, 0x61, 0xd0, 0x78, 0xf2, 0x2c, 0x6f, - 0x40, 0xc7, 0xd4, 0x6b, 0xaf, 0x9e, 0xa1, 0x34, 0x28, 0xf6, 0x11, 0x2c, 0xcd, 0x1b, 0xb4, 0x6c, - 0x7c, 0xed, 0x0c, 0xf5, 0x70, 0xce, 0x7e, 0xe1, 0x29, 0xab, 0xbd, 0xa0, 0xf1, 0x19, 0x27, 0x54, - 0x21, 0xd0, 0x4f, 0xd5, 0xfe, 0xd3, 0xab, 0x67, 0xfd, 0x54, 0xed, 0x4b, 0x8d, 0xa1, 0x13, 0x66, - 0x0f, 0xc3, 0x34, 0x93, 0xe3, 0xeb, 0xe6, 0xd4, 0xa3, 0x2e, 0x7a, 0x5f, 0x61, 0x86, 0xe6, 0x7f, - 0xfc, 0x9a, 0xb9, 0x43, 0x4a, 0x87, 0xc1, 0x6d, 0x68, 0xeb, 0x5a, 0xf6, 0xca, 0x19, 0x8d, 0xd6, - 0xf7, 0x3f, 0x1c, 0x4d, 0xc1, 0xbe, 0x0e, 0x1d, 0x2a, 0x64, 0x8a, 0x64, 0xfc, 0xf6, 0xa2, 0x04, - 0xa8, 0x6a, 0xa2, 0xd3, 0x8e, 0x54, 0x55, 0xf1, 0x5d, 0xe8, 0x18, 0xe7, 0xc3, 0x5e, 0x94, 0x6a, - 0xed, 0x84, 0x38, 0x86, 0x82, 0xdd, 0x84, 0xd6, 0x0c, 0xed, 0xd8, 0xf8, 0x6b, 0x8b, 0x1a, 0xaa, - 0xcc, 0x9b, 0xc2, 0xb2, 0xbb, 0xd0, 0xcb, 0xc8, 0xef, 0x54, 0xa2, 0x7b, 0xc3, 0x14, 0x01, 0xcb, - 0x3b, 0xf4, 0xc6, 0x29, 0x75, 0x20, 0x2b, 0x1d, 0xd4, 0xdf, 0x83, 0xeb, 0xd5, 0x0a, 0xa2, 0x29, - 0x2f, 0xea, 0xb8, 0xed, 0x26, 0xcd, 0xf2, 0xf6, 0x39, 0x12, 0x36, 0x5f, 0x88, 0x74, 0xae, 0x25, - 0x17, 0x54, 0x28, 0xef, 0x16, 0xa7, 0x04, 0x2a, 0xe5, 0xf8, 0xd6, 0x99, 0x65, 0x15, 0xe7, 0x8c, - 0x39, 0x3b, 0xe8, 0x78, 0xfa, 0x18, 0xfa, 0x93, 0xfc, 0xf9, 0xf3, 0x53, 0x2d, 0x23, 0xe3, 0x77, - 0x68, 0x5c, 0x25, 0x82, 0xaa, 0x14, 0x2d, 0x9d, 0xde, 0xa4, 0x52, 0xc1, 0xbc, 0x06, 0x1d, 0x3f, - 0x76, 0xbd, 0x20, 0x48, 0xc7, 0xab, 0xaa, 0x68, 0xe9, 0xc7, 0x1b, 0x41, 0x40, 0xb7, 0xe7, 0x45, - 0xc2, 0xe9, 0x5a, 0xab, 0x1b, 0x06, 0xe3, 0xaf, 0xab, 0xf3, 0xca, 0x80, 0xb6, 0x02, 0xba, 0x5e, - 0x6f, 0xc2, 0x8e, 0x30, 0x18, 0xdf, 0xd6, 0xd7, 0xeb, 0x35, 0x68, 0x2b, 0x40, 0x3f, 0x74, 0xe6, - 0x9d, 0xb8, 0x06, 0x32, 0x7e, 0x57, 0xc5, 0xa2, 0x33, 0xef, 0x64, 0x4f, 0x83, 0x50, 0xb7, 0xd5, - 0x95, 0x25, 0xb2, 0x76, 0xef, 0x2d, 0xea, 0x76, 0x91, 0xc4, 0x70, 0xba, 0x61, 0x91, 0xcf, 0x20, - 0x7b, 0x40, 0x16, 0xcc, 0x8d, 0xd6, 0xc7, 0xef, 0x9f, 0xb5, 0x07, 0x3a, 0x47, 0x83, 0xf6, 0xc0, - 0xa4, 0x6b, 0xd6, 0x01, 0x94, 0xa9, 0xa3, 0xcd, 0x5e, 0x5b, 0x1c, 0x53, 0x04, 0x07, 0x8e, 0xba, - 0xaf, 0x43, 0x5b, 0xbd, 0x0e, 0x40, 0x17, 0x7f, 0xd4, 0x98, 0x3b, 0x8b, 0x63, 0x0a, 0x67, 0xdf, - 0xe9, 0x3e, 0x2d, 0xfc, 0xfe, 0x3b, 0xd0, 0xcd, 0xd1, 0xad, 0x47, 0xc7, 0x7a, 0xfc, 0xc1, 0xa2, - 0x0e, 0x18, 0x8f, 0xdf, 0xb1, 0x72, 0xdd, 0xc2, 0x8f, 0xd0, 0x91, 0x45, 0xde, 0xcb, 0xf8, 0xc3, - 0xc5, 0x8f, 0x14, 0x61, 0x81, 0x43, 0x27, 0x9b, 0x8a, 0x10, 0xee, 0x42, 0x4f, 0x31, 0x4d, 0x0d, - 0x5a, 0x5f, 0x94, 0x91, 0xd2, 0x1d, 0x72, 0x14, 0x77, 0xd5, 0xb0, 0x9b, 0xd0, 0xf2, 0x92, 0x24, - 0x3a, 0x1d, 0x7f, 0xb4, 0xa8, 0x18, 0x1b, 0x08, 0x76, 0x14, 0x16, 0x45, 0x69, 0x96, 0x47, 0x32, - 0x34, 0x57, 0x6c, 0xbe, 0xb1, 0x28, 0x4a, 0x95, 0x1b, 0x88, 0x4e, 0x6f, 0x56, 0xb9, 0x8e, 0xf8, - 0x1e, 0x58, 0x89, 0xc8, 0xa4, 0x1b, 0xcc, 0xa2, 0xf1, 0xdd, 0x33, 0xa7, 0x8f, 0xba, 0x5a, 0xe2, - 0x74, 0x12, 0x7d, 0x37, 0x67, 0xee, 0x5e, 0xec, 0x37, 0xe7, 0xef, 0xc5, 0xfe, 0xa4, 0x69, 0x2d, - 0x8f, 0x98, 0x7d, 0x17, 0xfa, 0x1b, 0xf4, 0x78, 0x24, 0xcc, 0xc8, 0x62, 0xde, 0x84, 0x66, 0x91, - 0x70, 0x2b, 0x4c, 0x31, 0x51, 0x3c, 0xe7, 0x5b, 0xf1, 0x44, 0x38, 0x84, 0xb6, 0xff, 0xa1, 0x09, - 0xed, 0x7d, 0x91, 0xa7, 0x3e, 0xff, 0xe2, 0x1b, 0x5a, 0x6f, 0x18, 0xc1, 0x88, 0xcb, 0xb2, 0xbd, - 0x92, 0x01, 0x42, 0x2f, 0x16, 0x1c, 0xbb, 0x65, 0x2e, 0xef, 0x32, 0xb4, 0x54, 0x70, 0xa7, 0x6e, - 0xf6, 0xa8, 0x0e, 0x29, 0x45, 0x9e, 0x1d, 0x05, 0xe2, 0x59, 0x8c, 0x4a, 0xd1, 0xa2, 0x8b, 0x31, - 0x60, 0x40, 0x5b, 0x01, 0x05, 0xeb, 0x86, 0x80, 0xb4, 0xae, 0xad, 0x3c, 0x7c, 0x03, 0x24, 0xdd, - 0x33, 0x79, 0xc2, 0xce, 0x05, 0x79, 0xc2, 0x37, 0xa1, 0x19, 0x9b, 0x1b, 0x25, 0x05, 0x9e, 0x9e, - 0x1e, 0x10, 0x9c, 0xdd, 0x86, 0xe2, 0x5a, 0x99, 0x76, 0x3e, 0x2e, 0xbe, 0x76, 0xb6, 0x0e, 0xdd, - 0xe2, 0xb9, 0x91, 0xf6, 0x37, 0x2e, 0xaf, 0x95, 0x0f, 0x90, 0x0e, 0x4c, 0xcb, 0x29, 0xc9, 0xce, - 0x49, 0x1d, 0xaa, 0xaa, 0x0b, 0xf1, 0xa9, 0xf7, 0x32, 0xa9, 0x43, 0x2a, 0xc5, 0x98, 0xb4, 0x69, - 0x98, 0xb9, 0xbe, 0x88, 0x33, 0xa9, 0xd3, 0x11, 0x9d, 0x30, 0xdb, 0xc4, 0x2e, 0xfb, 0x36, 0x0c, - 0x52, 0xee, 0x3f, 0x75, 0x67, 0xd9, 0x54, 0x7d, 0x62, 0x50, 0xbd, 0xa8, 0x3a, 0xcb, 0xa6, 0x9f, - 0x70, 0x0f, 0x8f, 0x60, 0x15, 0xf3, 0xf4, 0x90, 0x76, 0x27, 0x9b, 0xd2, 0xac, 0xef, 0xc2, 0xf2, - 0x8c, 0xcf, 0x0e, 0x79, 0x9a, 0x1d, 0x85, 0x89, 0xb1, 0x8e, 0x43, 0xca, 0x18, 0x8e, 0x4a, 0x84, - 0x5a, 0x8b, 0xfd, 0xc7, 0x35, 0xb0, 0x90, 0x8b, 0x28, 0x4b, 0x8c, 0x41, 0x73, 0xe6, 0x27, 0xb9, - 0x76, 0x79, 0xa9, 0xad, 0x9f, 0x30, 0x29, 0x29, 0xd1, 0x4f, 0x98, 0x68, 0x0f, 0x1b, 0x2a, 0x45, - 0x88, 0x6d, 0xf5, 0xdc, 0xe1, 0x94, 0xf2, 0x30, 0x4a, 0x32, 0x4c, 0x97, 0x5d, 0x81, 0xb6, 0x1f, - 0x53, 0x3c, 0xab, 0xee, 0x19, 0xb5, 0xfc, 0x18, 0xe3, 0x58, 0x05, 0x2e, 0xaf, 0x37, 0xb4, 0xfc, - 0x78, 0x2b, 0x38, 0xb1, 0xff, 0xa6, 0x06, 0xcb, 0x7b, 0xa9, 0xf0, 0x79, 0x96, 0x6d, 0xe3, 0x91, - 0xed, 0x91, 0xcf, 0xc5, 0xa0, 0x49, 0x79, 0x34, 0xf5, 0x76, 0x80, 0xda, 0x28, 0xc3, 0x2a, 0xd9, - 0x50, 0x04, 0x16, 0x0d, 0xa7, 0x4b, 0x10, 0x8a, 0x2b, 0x0a, 0x34, 0x0d, 0x6c, 0x54, 0xd0, 0x94, - 0x81, 0xbb, 0x09, 0xc3, 0xf2, 0xd2, 0x4f, 0x25, 0x29, 0x58, 0x5e, 0xc5, 0xa6, 0x59, 0xde, 0x82, - 0x5e, 0x4a, 0x5c, 0x56, 0xd3, 0xa8, 0x04, 0x21, 0x28, 0x10, 0xce, 0x63, 0x1f, 0xc1, 0x68, 0x2f, - 0xe5, 0x89, 0x97, 0x72, 0xb4, 0xee, 0x33, 0xe2, 0xe1, 0x55, 0x68, 0x47, 0x3c, 0x9e, 0xca, 0x23, - 0xbd, 0x5e, 0xdd, 0x2b, 0x9e, 0x97, 0xd5, 0x2b, 0xcf, 0xcb, 0x90, 0x97, 0x29, 0xf7, 0xf4, 0x2b, - 0x34, 0x6a, 0xa3, 0x8e, 0xc5, 0x79, 0xa4, 0x73, 0x7b, 0x96, 0xa3, 0x3a, 0xf6, 0xcf, 0x1b, 0xd0, - 0xd3, 0x9c, 0xa1, 0xaf, 0xa8, 0x5d, 0xa9, 0x15, 0xbb, 0x32, 0x82, 0x46, 0xf6, 0x24, 0xd2, 0xdb, - 0x84, 0x4d, 0xf6, 0x11, 0x34, 0xa2, 0x70, 0xa6, 0xc3, 0x92, 0xd7, 0xe6, 0xce, 0x8a, 0x79, 0xfe, - 0x6a, 0x11, 0x42, 0x6a, 0x34, 0x50, 0x74, 0xeb, 0x1b, 0x85, 0x55, 0xf3, 0x04, 0xed, 0xf6, 0x09, - 0x6a, 0x04, 0x32, 0xd5, 0xf3, 0xe9, 0xde, 0xa4, 0x51, 0xf3, 0x81, 0xd3, 0xd5, 0x90, 0xad, 0x80, - 0x7d, 0x03, 0xac, 0x2c, 0xf6, 0x92, 0xec, 0x48, 0xc8, 0x22, 0x10, 0x91, 0x27, 0xf1, 0xda, 0xe6, - 0xee, 0xc1, 0x49, 0xbc, 0xaf, 0x31, 0xfa, 0x63, 0x05, 0x25, 0xfb, 0x01, 0xf4, 0x33, 0x9e, 0x65, - 0xea, 0x22, 0xef, 0x44, 0x68, 0xf5, 0xbf, 0x52, 0x8d, 0x31, 0x08, 0x8b, 0xff, 0xda, 0x08, 0x7b, - 0x56, 0x82, 0xd8, 0x27, 0x30, 0x34, 0xe3, 0x23, 0x31, 0x9d, 0x16, 0x49, 0xc8, 0xd7, 0xce, 0xcc, - 0xb0, 0x4d, 0xe8, 0xca, 0x3c, 0x83, 0xac, 0x8a, 0x60, 0x3f, 0x86, 0x61, 0xa2, 0x36, 0xd3, 0xd5, - 0x59, 0x76, 0x65, 0x46, 0xae, 0xcf, 0xb9, 0x36, 0x73, 0x9b, 0x5d, 0x5e, 0xce, 0x2b, 0xe1, 0x99, - 0xfd, 0xe7, 0x75, 0xe8, 0x55, 0x56, 0x4d, 0x8f, 0xfe, 0x32, 0x9e, 0x9a, 0xa4, 0x3a, 0xb6, 0x11, - 0x76, 0x24, 0xf4, 0xeb, 0x97, 0xae, 0x43, 0x6d, 0x84, 0xa5, 0x42, 0x17, 0x6a, 0xba, 0x0e, 0xb5, - 0xd1, 0x74, 0xea, 0xe0, 0x51, 0xbd, 0x0f, 0xa0, 0x4d, 0x69, 0x3a, 0xfd, 0x12, 0xb8, 0x15, 0xd0, - 0xeb, 0x40, 0x4f, 0x7a, 0x87, 0x5e, 0x66, 0x6a, 0x00, 0x45, 0x1f, 0x55, 0xf3, 0x29, 0x4f, 0x71, - 0x2d, 0xda, 0xea, 0x9a, 0x2e, 0xee, 0x35, 0x59, 0xb3, 0xe7, 0x22, 0x56, 0x4f, 0x48, 0xfa, 0x8e, - 0x85, 0x80, 0xcf, 0x44, 0x4c, 0xc3, 0xf4, 0xce, 0x12, 0x3f, 0xbb, 0x8e, 0xe9, 0xa2, 0xcd, 0x7a, - 0x92, 0x73, 0x74, 0xff, 0x02, 0xba, 0x45, 0xd4, 0x75, 0x3a, 0xd4, 0xdf, 0x0a, 0xd8, 0x6d, 0xa0, - 0xab, 0x78, 0xee, 0x33, 0x2f, 0x94, 0x24, 0x42, 0x22, 0x97, 0x64, 0x5e, 0x1b, 0xce, 0x12, 0x22, - 0x3e, 0xf5, 0x42, 0x79, 0xa0, 0xc0, 0xf6, 0x7f, 0xd4, 0x60, 0xf9, 0xcc, 0xc6, 0xa0, 0x67, 0x86, - 0x9b, 0x62, 0xee, 0x57, 0xf6, 0x9d, 0x36, 0x76, 0xb7, 0x02, 0x42, 0xc8, 0x19, 0x09, 0x5e, 0x5d, - 0x23, 0xe4, 0x0c, 0xa5, 0xee, 0x0a, 0xb4, 0xe5, 0x09, 0x71, 0x46, 0x29, 0x51, 0x4b, 0x9e, 0x20, - 0x4b, 0x36, 0x30, 0xca, 0x9d, 0xba, 0x11, 0x7f, 0xca, 0x23, 0xe2, 0xd9, 0x70, 0xfd, 0xc6, 0x0b, - 0x24, 0x62, 0x6d, 0x5b, 0x4c, 0xb7, 0x91, 0x16, 0xe3, 0x5e, 0xd5, 0xb2, 0x7f, 0x02, 0x96, 0x81, - 0xb2, 0x2e, 0xb4, 0xee, 0xf3, 0xc3, 0x7c, 0x3a, 0x7a, 0x85, 0x59, 0xd0, 0xc4, 0x11, 0xa3, 0x1a, - 0xb6, 0x3e, 0xf5, 0xd2, 0x78, 0x54, 0x47, 0xf4, 0x83, 0x34, 0x15, 0xe9, 0xa8, 0x81, 0xcd, 0x3d, - 0x2f, 0x0e, 0xfd, 0x51, 0x13, 0x9b, 0x0f, 0x3d, 0xe9, 0x45, 0xa3, 0x96, 0xfd, 0x8b, 0x16, 0x58, - 0x7b, 0xfa, 0xeb, 0xec, 0x3e, 0x0c, 0x8a, 0x37, 0x9a, 0xe7, 0x67, 0x56, 0xf6, 0x16, 0x1b, 0x94, - 0x59, 0xe9, 0x27, 0x95, 0xde, 0xe2, 0x4b, 0xcf, 0xfa, 0x99, 0x97, 0x9e, 0xaf, 0x43, 0xe3, 0x49, - 0x7a, 0x3a, 0x5f, 0x77, 0xdb, 0x8b, 0xbc, 0xd8, 0x41, 0x30, 0xfb, 0x10, 0x7a, 0x28, 0x23, 0x6e, - 0x46, 0x4e, 0x83, 0xce, 0x46, 0x54, 0xdf, 0xd3, 0x12, 0xdc, 0x01, 0x24, 0xd2, 0x8e, 0xc5, 0x1a, - 0x58, 0xfe, 0x51, 0x18, 0x05, 0x29, 0x8f, 0x75, 0x4d, 0x9b, 0x9d, 0x5d, 0xb2, 0x53, 0xd0, 0xb0, - 0x1f, 0xc1, 0x28, 0x2c, 0xb3, 0x29, 0x65, 0xa9, 0x61, 0x4e, 0xbd, 0x2b, 0xf9, 0x16, 0x67, 0xa9, - 0x42, 0x4e, 0x27, 0x59, 0x79, 0xf1, 0xbd, 0x53, 0xbd, 0xf8, 0xae, 0xde, 0xf3, 0xd1, 0x71, 0x63, - 0x15, 0xb1, 0x18, 0x9e, 0x36, 0xb7, 0xb4, 0x8f, 0xd0, 0x5d, 0xf4, 0x42, 0xcd, 0x09, 0xa7, 0x7d, - 0x85, 0x1b, 0x30, 0x44, 0xdf, 0xc3, 0x55, 0x2e, 0x0b, 0x9a, 0x1d, 0xd0, 0xaf, 0x6e, 0xf2, 0xec, - 0xe8, 0x3e, 0x3a, 0x2d, 0x28, 0x8c, 0x37, 0x61, 0x68, 0xfe, 0x8b, 0xbe, 0x2e, 0xde, 0xd3, 0xa5, - 0x08, 0x0d, 0x55, 0x57, 0xc5, 0xd7, 0xe0, 0x92, 0x7f, 0xe4, 0xc5, 0x31, 0x8f, 0xdc, 0xc3, 0x7c, - 0x32, 0x31, 0xa7, 0x45, 0x9f, 0x92, 0x78, 0xcb, 0x1a, 0x75, 0x8f, 0x30, 0x74, 0xf8, 0xd8, 0x30, - 0x88, 0xc3, 0x48, 0x65, 0xaa, 0xe9, 0x64, 0x1c, 0x10, 0x65, 0x2f, 0x0e, 0x23, 0x4a, 0x55, 0xe3, - 0xf9, 0xf8, 0x43, 0x18, 0xe5, 0x79, 0x18, 0x64, 0xae, 0x14, 0xe6, 0x29, 0xa4, 0xce, 0x77, 0x56, - 0x32, 0x0d, 0x8f, 0xf3, 0x30, 0x38, 0x10, 0xfa, 0x31, 0xe4, 0x80, 0xe8, 0x4d, 0xd7, 0xfe, 0x21, - 0xf4, 0xab, 0xb2, 0x83, 0xb2, 0x48, 0xa1, 0xe0, 0xe8, 0x15, 0x06, 0xd0, 0xde, 0x15, 0xe9, 0xcc, - 0x8b, 0x46, 0x35, 0x6c, 0xab, 0xe7, 0x20, 0xa3, 0x3a, 0xeb, 0x83, 0x65, 0x62, 0x94, 0x51, 0xc3, - 0xfe, 0x2e, 0x58, 0xe6, 0x6d, 0x27, 0x3d, 0xaa, 0x13, 0x01, 0x57, 0xbe, 0x9b, 0xb2, 0x62, 0x16, - 0x02, 0xc8, 0x6f, 0x33, 0x4f, 0x9a, 0xeb, 0xe5, 0x93, 0x66, 0xfb, 0x77, 0xa0, 0x5f, 0x5d, 0x9c, - 0x49, 0x9c, 0xd5, 0xca, 0xc4, 0xd9, 0x39, 0xa3, 0xa8, 0x26, 0x95, 0x8a, 0x99, 0x5b, 0x71, 0x2f, - 0x2c, 0x04, 0xe0, 0x67, 0xec, 0x3f, 0xaa, 0x41, 0x8b, 0x7c, 0x76, 0x3a, 0x86, 0xb0, 0x51, 0xea, - 0x4e, 0xcb, 0xe9, 0x12, 0xe4, 0x7f, 0x71, 0x95, 0xae, 0x28, 0x90, 0x34, 0x5f, 0x58, 0x20, 0xb9, - 0xfd, 0x04, 0xda, 0xea, 0x15, 0x39, 0x5b, 0x86, 0xc1, 0xe3, 0xf8, 0x38, 0x16, 0xcf, 0x62, 0x05, - 0x18, 0xbd, 0xc2, 0x2e, 0xc1, 0x92, 0x61, 0xba, 0x7e, 0xae, 0x3e, 0xaa, 0xb1, 0x11, 0xf4, 0x69, - 0x5b, 0x0d, 0xa4, 0xce, 0x5e, 0x87, 0xb1, 0x3e, 0x48, 0xee, 0x8b, 0x98, 0xef, 0x0a, 0x19, 0x4e, - 0x4e, 0x0d, 0xb6, 0xc1, 0x96, 0xa0, 0xb7, 0x2f, 0x45, 0xb2, 0xcf, 0xe3, 0x20, 0x8c, 0xa7, 0xa3, - 0xe6, 0xed, 0x87, 0xd0, 0x56, 0x8f, 0xdb, 0x2b, 0x9f, 0x54, 0x80, 0xd1, 0x2b, 0x48, 0x8d, 0x56, - 0x35, 0x8c, 0xa7, 0xbb, 0xfc, 0x44, 0x2a, 0xa3, 0xb4, 0xed, 0x65, 0x72, 0x54, 0x67, 0x43, 0x00, - 0x3d, 0xeb, 0x83, 0x38, 0x18, 0x35, 0xee, 0x6d, 0xfe, 0xf2, 0x57, 0x6f, 0xd6, 0xfe, 0xe9, 0x57, - 0x6f, 0xd6, 0xfe, 0xed, 0x57, 0x6f, 0xbe, 0xf2, 0xb3, 0x7f, 0x7f, 0xb3, 0xf6, 0xd9, 0x87, 0x95, - 0xa7, 0xfb, 0x33, 0x4f, 0xa6, 0xe1, 0x89, 0xaa, 0xea, 0x99, 0x4e, 0xcc, 0xef, 0x24, 0xc7, 0xd3, - 0x3b, 0xc9, 0xe1, 0x1d, 0x23, 0x73, 0x87, 0x6d, 0x7a, 0x91, 0xff, 0xd1, 0xff, 0x04, 0x00, 0x00, - 0xff, 0xff, 0x3c, 0x6c, 0x1c, 0xfc, 0x10, 0x40, 0x00, 0x00, + // 6213 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x7c, 0x4b, 0x6f, 0x1c, 0x49, + 0x72, 0xf0, 0xf4, 0xbb, 0x3a, 0xfa, 0xc1, 0x66, 0xea, 0xc1, 0x1e, 0x69, 0x1e, 0x9c, 0x5a, 0x49, + 0xc3, 0xd5, 0xcc, 0x50, 0x33, 0x9c, 0xd1, 0xee, 0xec, 0x7b, 0x29, 0x4a, 0xda, 0xe1, 0x2e, 0x29, + 0xf1, 0x2b, 0x52, 0xdf, 0x00, 0x73, 0xf8, 0x0a, 0xc5, 0xaa, 0xec, 0x66, 0x0d, 0xab, 0x2b, 0x4b, + 0x55, 0x59, 0x12, 0xa9, 0xd3, 0xf7, 0xe1, 0xf3, 0xc9, 0x7f, 0xc0, 0x80, 0x4f, 0x0b, 0xc3, 0x80, + 0xbd, 0x36, 0x8c, 0x35, 0x60, 0xd8, 0x17, 0x9f, 0x0d, 0xec, 0xd1, 0x27, 0x1f, 0x7c, 0x30, 0x8c, + 0xf5, 0xd1, 0x80, 0x6f, 0x06, 0xf6, 0x62, 0xc0, 0x88, 0xc8, 0xcc, 0xaa, 0xea, 0x26, 0xa9, 0x79, + 0xd8, 0xd8, 0x8b, 0xf7, 0xc4, 0xcc, 0x88, 0xc8, 0xac, 0xec, 0xc8, 0xc8, 0x78, 0x66, 0x12, 0x86, + 0x49, 0x98, 0xf0, 0x28, 0x8c, 0xf9, 0x7a, 0x92, 0x0a, 0x29, 0x98, 0x65, 0xfa, 0xd7, 0xde, 0x9b, + 0x86, 0xf2, 0x28, 0x3f, 0x5c, 0xf7, 0xc5, 0xec, 0xce, 0x54, 0x4c, 0xc5, 0x1d, 0x22, 0x38, 0xcc, + 0x27, 0xd4, 0xa3, 0x0e, 0xb5, 0xd4, 0xc0, 0x6b, 0x10, 0x09, 0xff, 0xd8, 0xb4, 0x93, 0xc8, 0x8b, + 0x75, 0x7b, 0x49, 0x86, 0x33, 0x9e, 0x49, 0x6f, 0x96, 0x68, 0x40, 0x57, 0x9e, 0x68, 0x9c, 0xfd, + 0x77, 0x0d, 0xe8, 0xec, 0xf2, 0x2c, 0xf3, 0xa6, 0x9c, 0xd9, 0xd0, 0xc8, 0xc2, 0x60, 0x5c, 0x5b, + 0xad, 0xad, 0x0d, 0x37, 0x46, 0xeb, 0xc5, 0xb2, 0xf6, 0xa5, 0x27, 0xf3, 0xcc, 0x41, 0x24, 0xd2, + 0xf8, 0xb3, 0x60, 0x5c, 0x5f, 0xa4, 0xd9, 0xe5, 0xf2, 0x48, 0x04, 0x0e, 0x22, 0xd9, 0x08, 0x1a, + 0x3c, 0x4d, 0xc7, 0x8d, 0xd5, 0xda, 0x5a, 0xdf, 0xc1, 0x26, 0x63, 0xd0, 0x0c, 0x3c, 0xe9, 0x8d, + 0x9b, 0x04, 0xa2, 0x36, 0xbb, 0x01, 0xc3, 0x24, 0x15, 0xbe, 0x1b, 0xc6, 0x13, 0xe1, 0x12, 0xb6, + 0x45, 0xd8, 0x3e, 0x42, 0xb7, 0xe3, 0x89, 0xb8, 0x8f, 0x54, 0x63, 0xe8, 0x78, 0xb1, 0x17, 0x9d, + 0x66, 0x7c, 0xdc, 0x26, 0xb4, 0xe9, 0xb2, 0x21, 0xd4, 0xc3, 0x60, 0xdc, 0x59, 0xad, 0xad, 0x35, + 0x9d, 0x7a, 0x18, 0xe0, 0x37, 0xf2, 0x3c, 0x0c, 0xc6, 0x96, 0xfa, 0x06, 0xb6, 0x99, 0x0d, 0xfd, + 0x98, 0xf3, 0xe0, 0x91, 0x90, 0x0e, 0x4f, 0xa2, 0xd3, 0x71, 0x77, 0xb5, 0xb6, 0x66, 0x39, 0x73, + 0x30, 0x76, 0x0d, 0xac, 0x80, 0x1f, 0xe6, 0xd3, 0xdd, 0x6c, 0x3a, 0x86, 0xd5, 0xda, 0x5a, 0xd7, + 0x29, 0xfa, 0xec, 0x00, 0x56, 0x52, 0xfe, 0x34, 0xe7, 0x99, 0xe4, 0x81, 0x2b, 0xb9, 0x97, 0x06, + 0xe2, 0x79, 0xec, 0xce, 0x44, 0xc0, 0xc7, 0x3d, 0xe2, 0xc0, 0x6b, 0x55, 0x2e, 0xa5, 0xdc, 0x9b, + 0x1d, 0x68, 0xa2, 0x5d, 0x11, 0x70, 0xe7, 0x4a, 0x31, 0xb8, 0x0a, 0x66, 0x0e, 0x5c, 0xf5, 0x7c, + 0x9f, 0x27, 0x67, 0x27, 0xed, 0x7f, 0x89, 0x49, 0x2f, 0x9b, 0xb1, 0x55, 0xa8, 0xfd, 0x04, 0xba, + 0x5b, 0x22, 0x8e, 0xb9, 0x2f, 0x45, 0xca, 0xde, 0x84, 0x9e, 0x99, 0xc1, 0xd5, 0x1b, 0xda, 0x72, + 0xc0, 0x80, 0xb6, 0x03, 0xf6, 0x36, 0x2c, 0xf9, 0x86, 0xda, 0x0d, 0xe3, 0x80, 0x9f, 0xd0, 0x8e, + 0xb6, 0x9c, 0x61, 0x01, 0xde, 0x46, 0xa8, 0xfd, 0x87, 0x0d, 0xe8, 0xec, 0x1f, 0xe5, 0x93, 0x49, + 0xc4, 0xd9, 0x0d, 0x18, 0xe8, 0xe6, 0x96, 0x88, 0xb6, 0x83, 0x13, 0x3d, 0xef, 0x3c, 0x90, 0xad, + 0x42, 0x4f, 0x03, 0x0e, 0x4e, 0x13, 0xae, 0xa7, 0xad, 0x82, 0xe6, 0xe7, 0xd9, 0x0d, 0x63, 0x12, + 0x94, 0x86, 0x33, 0x0f, 0x5c, 0xa0, 0xf2, 0x4e, 0x48, 0x76, 0xe6, 0xa9, 0x3c, 0xfa, 0xda, 0x66, + 0x14, 0x3e, 0xe3, 0x0e, 0x9f, 0x6e, 0xc5, 0x92, 0x24, 0xa8, 0xe5, 0x54, 0x41, 0x6c, 0x03, 0xae, + 0x64, 0x6a, 0x88, 0x9b, 0x7a, 0xf1, 0x94, 0x67, 0x6e, 0x1e, 0xc6, 0xf2, 0x5b, 0x1f, 0x8d, 0xdb, + 0xab, 0x8d, 0xb5, 0xa6, 0x73, 0x49, 0x23, 0x1d, 0xc2, 0x3d, 0x21, 0x14, 0x7b, 0x1f, 0x2e, 0x2f, + 0x8c, 0x51, 0x43, 0x3a, 0xab, 0x8d, 0xb5, 0x86, 0xc3, 0xe6, 0x86, 0x6c, 0xd3, 0x88, 0x07, 0xb0, + 0x9c, 0xe6, 0x31, 0x9e, 0xb3, 0x87, 0x61, 0x24, 0x79, 0xba, 0x9f, 0x70, 0x9f, 0x24, 0xb1, 0xb7, + 0xb1, 0xb2, 0x4e, 0x47, 0xd1, 0x59, 0x44, 0x3b, 0x67, 0x47, 0xb0, 0x77, 0x0b, 0xe6, 0x3d, 0x38, + 0x49, 0x52, 0x12, 0xd7, 0xde, 0x06, 0xa8, 0x09, 0x10, 0xe2, 0x54, 0xd1, 0xf6, 0x6f, 0xea, 0x60, + 0xdd, 0x0f, 0xb3, 0xc4, 0x93, 0xfe, 0x11, 0x5b, 0x81, 0xce, 0x24, 0x8f, 0xfd, 0x72, 0xbf, 0xdb, + 0xd8, 0xdd, 0x0e, 0xd8, 0xf7, 0x61, 0x29, 0x12, 0xbe, 0x17, 0xb9, 0xc5, 0xd6, 0x8e, 0xeb, 0xab, + 0x8d, 0xb5, 0xde, 0xc6, 0xa5, 0x52, 0xcc, 0x0a, 0xd1, 0x71, 0x86, 0x44, 0x5b, 0x8a, 0xd2, 0x0f, + 0x60, 0x94, 0xf2, 0x99, 0x90, 0xbc, 0x32, 0xbc, 0x41, 0xc3, 0x59, 0x39, 0xfc, 0xd3, 0xd4, 0x4b, + 0x1e, 0xa1, 0x6c, 0x2e, 0x29, 0xda, 0x72, 0xf8, 0x07, 0x15, 0xee, 0xf3, 0xa9, 0x1b, 0x06, 0x27, + 0x2e, 0x7d, 0x60, 0xdc, 0x5c, 0x6d, 0xac, 0xb5, 0x4a, 0x56, 0xf2, 0xe9, 0x76, 0x70, 0xb2, 0x83, + 0x18, 0xf6, 0x21, 0x5c, 0x5d, 0x1c, 0xa2, 0x66, 0x1d, 0xb7, 0x68, 0xcc, 0xa5, 0xb9, 0x31, 0x0e, + 0xa1, 0xd8, 0x5b, 0xd0, 0x37, 0x83, 0x24, 0x8a, 0x5d, 0x5b, 0x09, 0x42, 0x56, 0x11, 0xbb, 0x15, + 0xe8, 0x84, 0x99, 0x9b, 0x85, 0xf1, 0x31, 0x29, 0x0d, 0xcb, 0x69, 0x87, 0xd9, 0x7e, 0x18, 0x1f, + 0xb3, 0x57, 0xc1, 0x4a, 0xb9, 0xaf, 0x30, 0x16, 0x61, 0x3a, 0x29, 0xf7, 0x09, 0xb5, 0x02, 0xd8, + 0x74, 0x7d, 0xc9, 0xb5, 0xea, 0x68, 0xa7, 0xdc, 0xdf, 0x92, 0xdc, 0xce, 0xa0, 0xb5, 0xcb, 0xd3, + 0x29, 0x47, 0xed, 0x81, 0x03, 0xf7, 0x7d, 0x2f, 0x26, 0xbe, 0x5b, 0x4e, 0xd1, 0x47, 0xdd, 0x95, + 0x78, 0xa9, 0x0c, 0xbd, 0x88, 0x8e, 0x81, 0xe5, 0x98, 0x2e, 0xbb, 0x0e, 0xdd, 0x4c, 0x7a, 0xa9, + 0xc4, 0x5f, 0x47, 0xe2, 0xdf, 0x72, 0x2c, 0x02, 0xe0, 0x09, 0x5a, 0x81, 0x0e, 0x8f, 0x03, 0x42, + 0x35, 0xd5, 0x4e, 0xf2, 0x38, 0xd8, 0x0e, 0x4e, 0xec, 0xbf, 0xaa, 0xc1, 0x60, 0x37, 0x8f, 0x64, + 0xb8, 0x99, 0x4e, 0x73, 0x3e, 0x8b, 0x25, 0xea, 0xbc, 0xfb, 0x61, 0x26, 0xf5, 0x97, 0xa9, 0xcd, + 0xd6, 0xa0, 0xfb, 0x93, 0x54, 0xe4, 0x09, 0x49, 0x90, 0xda, 0xe9, 0xaa, 0x04, 0x95, 0x48, 0x94, + 0xb6, 0xc7, 0x69, 0xc0, 0xd3, 0x7b, 0xa7, 0x44, 0xdb, 0x38, 0x43, 0x5b, 0x45, 0xb3, 0xd7, 0xa0, + 0xbb, 0xcf, 0x13, 0x2f, 0xf5, 0x50, 0x04, 0x9a, 0xa4, 0x28, 0x4b, 0x00, 0xfe, 0x56, 0x22, 0xde, + 0x0e, 0xf4, 0x21, 0x34, 0x5d, 0x7b, 0x0a, 0xdd, 0xcd, 0xe9, 0x34, 0xe5, 0x53, 0x4f, 0x92, 0xd2, + 0x16, 0x09, 0x2d, 0xb7, 0xe1, 0xd4, 0x45, 0x42, 0x86, 0x01, 0x7f, 0x80, 0xe2, 0x0f, 0xb5, 0xd9, + 0x1b, 0xd0, 0xe4, 0xe7, 0xaf, 0x87, 0xe0, 0xec, 0x2a, 0xb4, 0x7d, 0x11, 0x4f, 0xc2, 0xa9, 0x36, + 0x27, 0xba, 0x67, 0xff, 0x59, 0x03, 0x5a, 0xf4, 0xe3, 0x90, 0xbd, 0xa8, 0xe2, 0x5d, 0xfe, 0xcc, + 0x8b, 0xcc, 0xae, 0x20, 0xe0, 0xc1, 0x33, 0x2f, 0x62, 0xab, 0xd0, 0xc2, 0x69, 0xb2, 0x73, 0x78, + 0xa3, 0x10, 0xec, 0x16, 0xb4, 0x50, 0x88, 0xb2, 0xf9, 0x15, 0xa0, 0x10, 0xdd, 0x6b, 0xfe, 0xea, + 0x9f, 0xde, 0x7c, 0xc5, 0x51, 0x68, 0xf6, 0x36, 0x34, 0xbd, 0xe9, 0x34, 0x23, 0x59, 0x9e, 0x3b, + 0x4e, 0xc5, 0xef, 0x75, 0x88, 0x80, 0xdd, 0x85, 0xae, 0xda, 0x37, 0xa4, 0x6e, 0x11, 0xf5, 0x4a, + 0xc5, 0x74, 0x56, 0xb7, 0xd4, 0x29, 0x29, 0x91, 0xe3, 0x61, 0xa6, 0x0f, 0x3c, 0x49, 0xb4, 0xe5, + 0x94, 0x00, 0xb4, 0x6d, 0x49, 0xca, 0x37, 0xa3, 0x48, 0xf8, 0xfb, 0xe1, 0x0b, 0xae, 0x2d, 0xe1, + 0x1c, 0x8c, 0xdd, 0x82, 0xe1, 0x9e, 0x12, 0x39, 0x87, 0x67, 0x79, 0x24, 0x33, 0x6d, 0x1d, 0x17, + 0xa0, 0x6c, 0x1d, 0xd8, 0x1c, 0xe4, 0x80, 0x7e, 0x7e, 0x77, 0xb5, 0xb1, 0x36, 0x70, 0xce, 0xc1, + 0xb0, 0x6f, 0xc0, 0x60, 0x8a, 0x9c, 0x0e, 0xe3, 0xa9, 0x3b, 0x89, 0x3c, 0x34, 0x9c, 0x0d, 0x34, + 0xac, 0x06, 0xf8, 0x30, 0xf2, 0xa6, 0x24, 0xe4, 0x49, 0x18, 0x45, 0xee, 0x8c, 0xcf, 0xc8, 0x5c, + 0x36, 0x1c, 0x8b, 0x00, 0xbb, 0x7c, 0x66, 0xff, 0x79, 0x13, 0xda, 0xdb, 0x71, 0xc6, 0x53, 0x89, + 0x47, 0xc8, 0x9b, 0x4c, 0xb8, 0x2f, 0xb9, 0x52, 0x5d, 0x4d, 0xa7, 0xe8, 0x23, 0x0b, 0x0e, 0xc4, + 0xa7, 0x69, 0x28, 0xf9, 0xfe, 0x87, 0x5a, 0x48, 0x4a, 0x00, 0xbb, 0x0d, 0xcb, 0x5e, 0x10, 0xb8, + 0x86, 0xda, 0x4d, 0xc5, 0xf3, 0x8c, 0x8e, 0x93, 0xe5, 0x2c, 0x79, 0x41, 0xb0, 0xa9, 0xe1, 0x8e, + 0x78, 0x9e, 0xb1, 0xb7, 0xa0, 0x91, 0xf2, 0x09, 0x89, 0x4c, 0x6f, 0x63, 0x49, 0x6d, 0xe9, 0xe3, + 0xc3, 0xcf, 0xb9, 0x2f, 0x1d, 0x3e, 0x71, 0x10, 0xc7, 0x2e, 0x43, 0xcb, 0x93, 0x32, 0x55, 0x5b, + 0xd4, 0x75, 0x54, 0x87, 0xad, 0xc3, 0x25, 0x3a, 0xb6, 0x32, 0x14, 0xb1, 0x2b, 0xbd, 0xc3, 0x08, + 0x6d, 0x6a, 0xa6, 0xcd, 0xc7, 0x72, 0x81, 0x3a, 0x40, 0xcc, 0x76, 0x90, 0xa1, 0xc1, 0x59, 0xa4, + 0x8f, 0xbd, 0x19, 0xcf, 0xc8, 0x7a, 0x74, 0x9d, 0x4b, 0xf3, 0x23, 0x1e, 0x21, 0x0a, 0xf9, 0x59, + 0x8e, 0xc1, 0x83, 0x6f, 0xd1, 0x19, 0xea, 0x17, 0x40, 0xd4, 0x0b, 0x57, 0xa0, 0x1d, 0x66, 0x2e, + 0x8f, 0x03, 0xad, 0x8b, 0x5a, 0x61, 0xf6, 0x20, 0x0e, 0xd8, 0x3b, 0xd0, 0x55, 0x5f, 0x09, 0xf8, + 0x84, 0x1c, 0x98, 0xde, 0xc6, 0x50, 0x4b, 0x2c, 0x82, 0xef, 0xf3, 0x89, 0x63, 0x49, 0xdd, 0x42, + 0xcf, 0x40, 0x0a, 0x97, 0x9f, 0x48, 0x9e, 0xc6, 0x5e, 0x44, 0xbb, 0x62, 0x39, 0x20, 0xc5, 0x03, + 0x0d, 0x61, 0x77, 0x61, 0xc5, 0x60, 0xdd, 0x4c, 0xce, 0xa4, 0x9b, 0xc7, 0xe1, 0x89, 0x1b, 0x7b, + 0xb1, 0x20, 0xe7, 0xa4, 0xe1, 0x5c, 0x36, 0xe8, 0x7d, 0x39, 0x93, 0x4f, 0xe2, 0xf0, 0xe4, 0x91, + 0x17, 0x0b, 0xb6, 0x06, 0xa3, 0x62, 0x98, 0x7c, 0x41, 0x3f, 0x78, 0x3c, 0x20, 0x1d, 0x31, 0x34, + 0xf0, 0x83, 0x17, 0xf8, 0x5b, 0x51, 0xbd, 0x57, 0x29, 0xc5, 0x64, 0x92, 0x71, 0xe9, 0x66, 0xdc, + 0x1f, 0x0f, 0xe9, 0x37, 0x5f, 0x2a, 0xe9, 0x1f, 0x13, 0x6e, 0x9f, 0xfb, 0xf6, 0xff, 0xaf, 0x41, + 0x8f, 0xce, 0xc5, 0x93, 0x24, 0x40, 0x35, 0xf2, 0x0d, 0x18, 0xcc, 0x6f, 0xba, 0x92, 0x9b, 0xbe, + 0x57, 0xdd, 0xf1, 0xab, 0xd0, 0xde, 0xf4, 0x91, 0x79, 0x24, 0x38, 0x03, 0x47, 0xf7, 0xd8, 0xb7, + 0x61, 0x29, 0xa7, 0x69, 0x5c, 0x5f, 0x9e, 0xb8, 0x11, 0xaa, 0x1f, 0x75, 0xd0, 0xb5, 0x54, 0xa8, + 0x6f, 0x6c, 0xc9, 0x13, 0x67, 0x90, 0x9b, 0xe6, 0x4e, 0x98, 0x49, 0xfb, 0x75, 0x68, 0x6d, 0xa6, + 0xa9, 0x77, 0x4a, 0x82, 0x82, 0x8d, 0x71, 0x8d, 0x2c, 0x92, 0xea, 0xd8, 0x3e, 0x34, 0x76, 0xbd, + 0x84, 0xdd, 0x84, 0xfa, 0x2c, 0x21, 0x4c, 0x6f, 0xe3, 0x4a, 0xe5, 0x94, 0x7b, 0xc9, 0xfa, 0x6e, + 0xf2, 0x20, 0x96, 0xe9, 0xa9, 0x53, 0x9f, 0x25, 0xd7, 0xee, 0x42, 0x47, 0x77, 0xd1, 0x5f, 0x3e, + 0xe6, 0xa7, 0xf4, 0x1b, 0xba, 0x0e, 0x36, 0xf1, 0x03, 0xcf, 0xbc, 0x28, 0x37, 0xee, 0x93, 0xea, + 0x7c, 0xb7, 0xfe, 0x71, 0xcd, 0xfe, 0xf7, 0x26, 0x58, 0xf7, 0x79, 0xc4, 0xe9, 0x97, 0xd8, 0xd0, + 0xaf, 0xca, 0xb8, 0xe1, 0xc2, 0x9c, 0xdc, 0xdb, 0xd0, 0x57, 0x36, 0x92, 0x46, 0x71, 0x7d, 0x88, + 0xe6, 0x60, 0xa8, 0xbc, 0xb7, 0xef, 0xe5, 0xfe, 0x31, 0x97, 0x74, 0x7a, 0x06, 0x8e, 0xe9, 0x22, + 0xe6, 0x91, 0xc6, 0x34, 0x15, 0x46, 0x77, 0xd9, 0x6b, 0x00, 0xa9, 0x78, 0xee, 0x86, 0xca, 0x50, + 0x29, 0x9d, 0x6f, 0xa5, 0xe2, 0xf9, 0x36, 0x9a, 0xaa, 0xdf, 0xca, 0xa1, 0xf9, 0x36, 0x8c, 0x2b, + 0x87, 0x06, 0xdd, 0x55, 0x37, 0x8c, 0xdd, 0x43, 0xf4, 0x86, 0xf4, 0xf9, 0x29, 0xe7, 0x24, 0x6f, + 0x76, 0x3b, 0xbe, 0x47, 0xae, 0x92, 0x56, 0x05, 0xdd, 0x97, 0xa8, 0x82, 0x73, 0x35, 0x0b, 0x9c, + 0xaf, 0x59, 0xee, 0x01, 0xec, 0xf3, 0xe9, 0x8c, 0xc7, 0x72, 0xd7, 0x4b, 0xc6, 0x3d, 0xda, 0x78, + 0xbb, 0xdc, 0x78, 0xb3, 0x5b, 0xeb, 0x25, 0x91, 0x92, 0x82, 0xca, 0x28, 0xf4, 0x5f, 0x7c, 0x2f, + 0x76, 0x65, 0x9a, 0xc7, 0xbe, 0x27, 0x55, 0x20, 0x60, 0x39, 0x3d, 0xdf, 0x8b, 0x0f, 0x34, 0xa8, + 0x72, 0xfc, 0x07, 0xd5, 0xe3, 0x7f, 0x0b, 0x96, 0x92, 0x34, 0x9c, 0x79, 0xe9, 0xa9, 0x7b, 0xcc, + 0x4f, 0x69, 0x33, 0xd4, 0x41, 0x1a, 0x68, 0xf0, 0xcf, 0xf8, 0xe9, 0x76, 0x70, 0x72, 0xed, 0x07, + 0xb0, 0xb4, 0xb0, 0x80, 0xaf, 0x24, 0x77, 0x3f, 0x6f, 0x40, 0x77, 0x2f, 0xe5, 0x5a, 0x65, 0xbf, + 0x09, 0xbd, 0xcc, 0x3f, 0xe2, 0x33, 0x4f, 0x9d, 0x74, 0x35, 0x03, 0x28, 0x10, 0x9d, 0xf2, 0x39, + 0xa5, 0x54, 0xff, 0x02, 0xa5, 0x34, 0x82, 0x86, 0xf2, 0x83, 0xf0, 0x30, 0x61, 0xb3, 0xd4, 0xc4, + 0xcd, 0xaa, 0x26, 0x5e, 0x85, 0xfe, 0x91, 0x97, 0xb9, 0x5e, 0x2e, 0x85, 0xeb, 0x8b, 0x88, 0x84, + 0xce, 0x72, 0xe0, 0xc8, 0xcb, 0x36, 0x73, 0x29, 0xb6, 0x44, 0xc4, 0x5e, 0x07, 0xf0, 0x45, 0xa4, + 0x95, 0x8a, 0x76, 0x02, 0xbb, 0xbe, 0x88, 0x94, 0x26, 0x41, 0xa9, 0xe4, 0x99, 0x0c, 0x67, 0x9e, + 0xde, 0x52, 0xd7, 0x17, 0x79, 0x2c, 0xc9, 0x72, 0x36, 0x9c, 0xe5, 0x02, 0xe5, 0x88, 0xe7, 0x5b, + 0x88, 0x60, 0xef, 0xc3, 0xd0, 0x17, 0xb3, 0xc4, 0x4d, 0x90, 0xb3, 0xe4, 0x93, 0x58, 0x67, 0x3c, + 0xf2, 0x3e, 0x52, 0xec, 0x1d, 0x73, 0xe5, 0x24, 0x6d, 0xc0, 0x92, 0x1f, 0xe5, 0x99, 0xe4, 0xa9, + 0x7b, 0xa8, 0x87, 0x9c, 0x75, 0xe2, 0x07, 0x9a, 0x44, 0x3b, 0x56, 0x36, 0x0c, 0xc2, 0xcc, 0x15, + 0x51, 0xe0, 0x2a, 0x75, 0xa3, 0xe5, 0xac, 0x17, 0x66, 0x8f, 0xa3, 0x40, 0x2b, 0x3c, 0x45, 0x13, + 0xf3, 0xe7, 0x86, 0xa6, 0x67, 0x68, 0x1e, 0xf1, 0xe7, 0x8a, 0xc6, 0xfe, 0x87, 0x3a, 0x74, 0xf6, + 0x44, 0x26, 0xef, 0xcf, 0x22, 0x23, 0xe2, 0xb5, 0xaf, 0x2a, 0xe2, 0xf5, 0xf3, 0x45, 0xfc, 0x1c, + 0x21, 0x6b, 0x9c, 0x23, 0x64, 0x68, 0x06, 0xaa, 0x74, 0x24, 0x1c, 0xca, 0x55, 0x1c, 0x96, 0x84, + 0x24, 0x20, 0xd7, 0xd1, 0xb7, 0x71, 0x03, 0xa5, 0x93, 0xd4, 0x46, 0x5a, 0x61, 0xa6, 0xf5, 0x91, + 0x42, 0x86, 0x24, 0x6b, 0xda, 0xf1, 0xb1, 0xc2, 0x4c, 0xcb, 0xde, 0x77, 0xe0, 0xd5, 0x62, 0xa4, + 0xfb, 0x3c, 0x94, 0x47, 0x22, 0x97, 0xee, 0x84, 0x62, 0xa8, 0x4c, 0x7b, 0xf6, 0x57, 0xcd, 0x4c, + 0x9f, 0x2a, 0xb4, 0x8a, 0xb0, 0xc8, 0x0f, 0x9b, 0xe4, 0x51, 0xe4, 0x4a, 0x7e, 0x22, 0xf5, 0x56, + 0x8e, 0x15, 0x6f, 0x34, 0xdf, 0x1e, 0xe6, 0x51, 0x74, 0xc0, 0x4f, 0x24, 0x2a, 0x7f, 0x6b, 0xa2, + 0x3b, 0xf6, 0x1f, 0x34, 0x01, 0x76, 0x84, 0x7f, 0x7c, 0xe0, 0xa5, 0x53, 0x2e, 0x31, 0x5e, 0x30, + 0x1a, 0x4d, 0x6b, 0xdc, 0x8e, 0x54, 0x7a, 0x8c, 0x6d, 0xc0, 0x55, 0xf3, 0xfb, 0x51, 0x0e, 0x31, + 0x76, 0x51, 0x2a, 0x49, 0x1f, 0x28, 0xa6, 0xb1, 0x2a, 0x56, 0x26, 0x7d, 0xc4, 0x3e, 0x2e, 0x79, + 0x8b, 0x63, 0xe4, 0x69, 0x42, 0xbc, 0x3d, 0xcf, 0xef, 0x1c, 0x94, 0xc3, 0x0f, 0x4e, 0x13, 0xf6, + 0x3e, 0x5c, 0x49, 0xf9, 0x24, 0xe5, 0xd9, 0x91, 0x2b, 0xb3, 0xea, 0xc7, 0x54, 0xd8, 0xb0, 0xac, + 0x91, 0x07, 0x59, 0xf1, 0xad, 0xf7, 0xe1, 0x8a, 0xe2, 0xd4, 0xe2, 0xf2, 0x94, 0xfe, 0x5e, 0x56, + 0xc8, 0xea, 0xea, 0x5e, 0x07, 0xca, 0x2a, 0x29, 0x9d, 0x6c, 0x9c, 0xd0, 0x88, 0x98, 0x71, 0x18, + 0x71, 0xf4, 0xcf, 0xb6, 0x8e, 0x30, 0x0e, 0xbe, 0xcf, 0x27, 0x9a, 0xf9, 0x25, 0x80, 0xd9, 0xd0, + 0xdc, 0x15, 0x01, 0x27, 0x56, 0x0f, 0x37, 0x86, 0xeb, 0x94, 0x9f, 0x42, 0x4e, 0x52, 0x22, 0x83, + 0x70, 0xec, 0x6d, 0xa0, 0xe9, 0x94, 0xf8, 0x9d, 0x3d, 0x2b, 0x16, 0x22, 0x49, 0x06, 0xdf, 0x87, + 0x2b, 0xe5, 0x4a, 0x5c, 0x4f, 0xba, 0xf2, 0x88, 0x93, 0x3a, 0x54, 0xc7, 0x65, 0xb9, 0x58, 0xd4, + 0xa6, 0x3c, 0x38, 0xe2, 0xa8, 0x1a, 0xd7, 0xa0, 0x23, 0x0e, 0x3f, 0x77, 0xf1, 0x20, 0xf4, 0xce, + 0x3f, 0x08, 0x6d, 0x71, 0xf8, 0xb9, 0xc3, 0x27, 0xec, 0x5b, 0x55, 0x53, 0xb2, 0xc0, 0x9a, 0x3e, + 0xb1, 0xe6, 0x72, 0x81, 0xaf, 0x70, 0xc7, 0xfe, 0x18, 0xda, 0xf8, 0x73, 0x1e, 0x27, 0x6c, 0x1d, + 0x3a, 0x92, 0xc4, 0x23, 0xd3, 0xa6, 0xff, 0x72, 0x69, 0x01, 0x4a, 0xd9, 0x71, 0x0c, 0x91, 0xed, + 0xc0, 0x52, 0xa1, 0x4e, 0x9f, 0xc4, 0xe1, 0xd3, 0x9c, 0xb3, 0x1f, 0xc1, 0x72, 0x92, 0x72, 0x2d, + 0xf6, 0x6e, 0x7e, 0x8c, 0xee, 0x89, 0x3e, 0xc1, 0x97, 0xb5, 0x94, 0x16, 0x23, 0x8e, 0x51, 0x42, + 0x87, 0xc9, 0x5c, 0xdf, 0xfe, 0x0c, 0x56, 0x0a, 0x8a, 0x7d, 0xee, 0x8b, 0x38, 0xf0, 0xd2, 0x53, + 0xb2, 0x7c, 0x0b, 0x73, 0x67, 0x5f, 0x65, 0xee, 0x7d, 0x9a, 0xfb, 0x4f, 0x6a, 0xd0, 0x7b, 0x98, + 0xbf, 0x78, 0x71, 0xaa, 0xce, 0x12, 0xeb, 0x43, 0xed, 0x11, 0x4d, 0x50, 0x77, 0x6a, 0x8f, 0xd0, + 0xd5, 0xda, 0x3b, 0xc6, 0x73, 0x4d, 0x72, 0xde, 0x75, 0x74, 0x0f, 0x23, 0xa9, 0xbd, 0xe3, 0x83, + 0x97, 0x48, 0xb4, 0x42, 0x63, 0x08, 0x70, 0x2f, 0x0f, 0x23, 0x74, 0x1d, 0xb4, 0xf0, 0x16, 0x7d, + 0x8c, 0x4d, 0xb6, 0x27, 0x6a, 0x29, 0x0f, 0x53, 0x31, 0x53, 0xcc, 0xd2, 0x2a, 0xe3, 0x1c, 0x8c, + 0xfd, 0x9b, 0x26, 0x58, 0x9f, 0x78, 0xd9, 0xd1, 0x4f, 0x45, 0x18, 0xb3, 0xf7, 0xa1, 0xfb, 0xb9, + 0x08, 0x63, 0x95, 0x14, 0x50, 0x89, 0xcd, 0x4b, 0x6a, 0x11, 0x8f, 0x44, 0xc0, 0xd7, 0x91, 0x06, + 0x57, 0xe3, 0x58, 0x9f, 0xeb, 0x96, 0xd6, 0xb4, 0x69, 0x38, 0x3d, 0x92, 0x2e, 0x02, 0xb5, 0x4a, + 0xec, 0x85, 0x99, 0x83, 0x30, 0x9a, 0xf5, 0x35, 0x40, 0xa3, 0x73, 0xe4, 0x8a, 0xd8, 0x4d, 0x8e, + 0x75, 0xc0, 0x61, 0x21, 0xe4, 0x71, 0xbc, 0x77, 0x8c, 0x47, 0x26, 0xcc, 0x5c, 0x9d, 0x7a, 0xa0, + 0x9f, 0x33, 0x17, 0xb7, 0xdd, 0x80, 0x21, 0x9a, 0xfa, 0xec, 0x38, 0x4c, 0xdc, 0x24, 0x15, 0x87, + 0xe6, 0xb7, 0xa0, 0x03, 0xb0, 0x7f, 0x1c, 0x26, 0x7b, 0x08, 0x23, 0x0b, 0xab, 0x13, 0x1a, 0xa8, + 0x6d, 0x95, 0x29, 0x03, 0x0d, 0x42, 0xb6, 0x50, 0xd6, 0x22, 0x52, 0xee, 0x6b, 0x87, 0x2c, 0x67, + 0x27, 0xe5, 0x11, 0xfa, 0xa9, 0x88, 0x42, 0x19, 0x26, 0x94, 0xa5, 0x50, 0xbe, 0x50, 0xa8, 0x6f, + 0x02, 0x44, 0x7c, 0x22, 0x5d, 0x14, 0x0e, 0x15, 0xe0, 0x2d, 0x64, 0x07, 0x10, 0xbb, 0x85, 0x48, + 0xf6, 0x0e, 0xf4, 0x14, 0x17, 0x14, 0x2d, 0x9c, 0xa1, 0x05, 0x42, 0x2b, 0xe2, 0xdb, 0xd0, 0x8b, + 0x45, 0xec, 0xf2, 0xa7, 0x44, 0xad, 0x8f, 0xdb, 0xdc, 0xc4, 0xb1, 0x88, 0x1f, 0x3c, 0x45, 0x62, + 0x76, 0x47, 0xaf, 0x41, 0xc5, 0xd8, 0xfd, 0x0b, 0x62, 0x6c, 0x5a, 0x89, 0x8a, 0x36, 0x3f, 0x30, + 0x2b, 0x51, 0x23, 0x06, 0x17, 0x8c, 0x50, 0xeb, 0x51, 0x43, 0x56, 0xa1, 0x4f, 0xfb, 0x3e, 0xf3, + 0x12, 0x57, 0x7a, 0x53, 0xed, 0x12, 0x01, 0xc2, 0x76, 0xbd, 0xe4, 0xc0, 0x9b, 0x32, 0x07, 0x5e, + 0xd5, 0xf9, 0x37, 0x6d, 0x3c, 0xdc, 0x43, 0x94, 0x38, 0xc5, 0xb5, 0x25, 0x13, 0xa3, 0x9f, 0x9f, + 0xb9, 0xbb, 0x3a, 0x97, 0xb9, 0x23, 0x49, 0xa5, 0x00, 0xe1, 0x8f, 0xeb, 0x60, 0xed, 0x08, 0x91, + 0x7c, 0x4d, 0xd1, 0xab, 0x6e, 0x69, 0xfd, 0xe2, 0x2d, 0x6d, 0xcc, 0x6f, 0xe9, 0x02, 0xeb, 0x9b, + 0x5f, 0x9e, 0xf5, 0xad, 0xaf, 0xcc, 0xfa, 0xf6, 0xd7, 0x60, 0x7d, 0x67, 0x91, 0xf5, 0x76, 0x07, + 0x5a, 0xfb, 0x5c, 0x3e, 0x4e, 0xec, 0x5f, 0x5a, 0xd0, 0xbd, 0xcf, 0x83, 0x5c, 0x31, 0xac, 0xfa, + 0xf3, 0x6b, 0x17, 0xff, 0xfc, 0xfa, 0xfc, 0xcf, 0x47, 0xfb, 0x61, 0x24, 0xfa, 0x9c, 0x94, 0x91, + 0x65, 0x04, 0x1a, 0x45, 0xbf, 0x94, 0x67, 0x9d, 0xb3, 0x99, 0x63, 0x53, 0x21, 0xce, 0x2f, 0x97, + 0x8d, 0xd6, 0xd7, 0x92, 0x8d, 0x05, 0xad, 0x70, 0x26, 0x9b, 0xf3, 0x85, 0x5c, 0x5b, 0xd4, 0x08, + 0xd6, 0x19, 0x8d, 0xb0, 0x03, 0x97, 0x44, 0xec, 0x06, 0x79, 0x12, 0x85, 0x18, 0x30, 0xb8, 0x9e, + 0x0a, 0x7e, 0xbb, 0xa6, 0xa6, 0x50, 0x88, 0xde, 0xe3, 0xf8, 0xbe, 0x21, 0x52, 0x21, 0xb1, 0xb3, + 0x2c, 0x16, 0x41, 0xa8, 0xa6, 0x02, 0xdc, 0x1a, 0x32, 0x87, 0xe4, 0xc8, 0xa9, 0xe2, 0x48, 0x9f, + 0xa0, 0x5b, 0x22, 0x22, 0x05, 0xff, 0x31, 0x2c, 0x95, 0x54, 0x4a, 0x46, 0x7a, 0x17, 0xc8, 0xc8, + 0xc0, 0x0c, 0x54, 0x62, 0xf2, 0xdb, 0xd0, 0x02, 0xef, 0xc1, 0x25, 0x13, 0xe9, 0x6b, 0x9b, 0x4e, + 0x3b, 0x38, 0x24, 0x09, 0x1a, 0xe9, 0xe0, 0x9e, 0xcc, 0x39, 0x6d, 0xd1, 0xf7, 0xe0, 0x72, 0x85, + 0x1c, 0x9d, 0xf7, 0xaa, 0x36, 0xa8, 0xca, 0xca, 0x72, 0x31, 0x16, 0xbb, 0x3b, 0x2a, 0x6b, 0xd9, + 0x0b, 0x78, 0x64, 0x3e, 0x34, 0x1e, 0xa9, 0xd8, 0x23, 0xe0, 0x91, 0xae, 0x8b, 0xec, 0xc2, 0x0d, + 0x74, 0xf1, 0x11, 0xef, 0x7b, 0x89, 0xcc, 0x53, 0xee, 0x26, 0x91, 0xe7, 0xf3, 0x23, 0x11, 0x05, + 0x3c, 0x2d, 0x17, 0xb7, 0x4c, 0x8b, 0x7b, 0x53, 0x44, 0xc1, 0x96, 0x88, 0xb6, 0x14, 0xe5, 0x5e, + 0x49, 0x68, 0xd6, 0xba, 0x09, 0x6f, 0x9c, 0x99, 0x0e, 0x0d, 0x47, 0x39, 0x11, 0xa3, 0x89, 0x5e, + 0x9d, 0x9f, 0x08, 0x49, 0xcc, 0x14, 0x1f, 0xc0, 0x15, 0xb5, 0x77, 0x4a, 0xb8, 0x8f, 0x39, 0x4f, + 0xdc, 0xc8, 0xcb, 0xe4, 0xf8, 0x92, 0xb2, 0xad, 0x84, 0x24, 0x01, 0xfe, 0x19, 0xe7, 0xc9, 0x8e, + 0xa7, 0xbe, 0xaa, 0x86, 0x68, 0xf7, 0x9b, 0xc6, 0xcc, 0xf1, 0xf6, 0xb2, 0xfa, 0x2a, 0x51, 0x29, + 0x1f, 0x1c, 0x07, 0x57, 0x98, 0xfc, 0x7d, 0xb8, 0x3e, 0x37, 0xc5, 0xcc, 0x4b, 0x8f, 0x4b, 0x7f, + 0x74, 0x7c, 0x85, 0xf8, 0xb6, 0x52, 0x19, 0xbf, 0x4b, 0x04, 0x6a, 0x06, 0xfb, 0xdf, 0x5a, 0x30, + 0x24, 0x3b, 0xfc, 0x3b, 0xb5, 0xf1, 0x3b, 0xb5, 0xf1, 0x3f, 0x40, 0x6d, 0xd8, 0xff, 0xb7, 0x06, + 0x9d, 0xbd, 0x54, 0x04, 0xb9, 0x2f, 0xbf, 0xa6, 0xa4, 0xcf, 0x4b, 0x50, 0xe3, 0x8b, 0x24, 0xa8, + 0x79, 0xc6, 0x5c, 0xff, 0xa2, 0x06, 0x5d, 0xbd, 0x84, 0x9d, 0x8d, 0xaf, 0xb9, 0x88, 0xb2, 0xa6, + 0x53, 0x3b, 0xb7, 0xa6, 0xf3, 0x85, 0xab, 0x40, 0xc1, 0x7a, 0xa6, 0xea, 0xd5, 0x22, 0x51, 0x3e, + 0x55, 0x4b, 0x09, 0x96, 0x82, 0x3e, 0x4e, 0x70, 0xef, 0xec, 0xe7, 0xd0, 0xa5, 0x80, 0x87, 0x34, + 0xc3, 0x55, 0x68, 0xa7, 0x54, 0xb4, 0xd0, 0x0b, 0xd5, 0xbd, 0x97, 0x9f, 0xd3, 0xfa, 0xd7, 0x73, + 0xfd, 0xfe, 0xb2, 0x06, 0x03, 0x8a, 0x3e, 0x1f, 0xe6, 0xb1, 0x3a, 0x09, 0x45, 0x0e, 0xab, 0x36, + 0x9f, 0xc3, 0x6a, 0xa6, 0x18, 0x24, 0xaa, 0xcf, 0xf4, 0xd5, 0x67, 0xb6, 0x44, 0x74, 0x9f, 0x4f, + 0x1c, 0xc2, 0x20, 0xab, 0xbc, 0x74, 0x9a, 0x9d, 0x57, 0xfe, 0x42, 0x38, 0xfe, 0xaa, 0xc4, 0x4b, + 0xbd, 0x59, 0x66, 0xca, 0x5f, 0xaa, 0xc7, 0x18, 0x34, 0xe9, 0xbc, 0x29, 0xb6, 0x50, 0x5b, 0x27, + 0x52, 0xb2, 0x30, 0x9e, 0x16, 0xca, 0xc3, 0xa2, 0xaa, 0xe7, 0x34, 0xe2, 0xf6, 0x26, 0x5c, 0x31, + 0x69, 0x7f, 0x3c, 0x94, 0x1b, 0x28, 0x71, 0x14, 0x2c, 0x9a, 0x99, 0x6a, 0x95, 0x99, 0x2e, 0x43, + 0xab, 0x7a, 0x4f, 0x40, 0x75, 0xec, 0x9b, 0xd0, 0x9b, 0x84, 0x11, 0xd7, 0x09, 0x37, 0x5c, 0x9a, + 0x4e, 0xbd, 0xd5, 0xa8, 0x52, 0xae, 0x7b, 0xf6, 0x5f, 0xd7, 0x60, 0x25, 0xf1, 0xd2, 0xa7, 0x39, + 0x97, 0x94, 0x76, 0xa3, 0x32, 0x91, 0x9b, 0x1d, 0x79, 0x69, 0x80, 0xe2, 0x49, 0x53, 0xa8, 0xd9, + 0x55, 0xe9, 0xba, 0x8b, 0x10, 0xb5, 0x96, 0x5b, 0xb0, 0x54, 0x19, 0x21, 0xbd, 0xd4, 0xa4, 0x52, + 0x06, 0xa9, 0x78, 0x4e, 0xd5, 0xbe, 0x7d, 0x04, 0x62, 0xd8, 0x56, 0xd2, 0x71, 0xd2, 0xe9, 0x54, + 0x01, 0x36, 0x54, 0x0f, 0xe2, 0x00, 0xe5, 0x33, 0xce, 0x67, 0x2a, 0xd3, 0xa0, 0x6e, 0x13, 0x74, + 0xe2, 0x7c, 0x46, 0xc9, 0x85, 0xcb, 0xd0, 0x3a, 0x3c, 0x95, 0xe4, 0x13, 0x23, 0x5c, 0x75, 0xec, + 0x7f, 0x6c, 0x42, 0xdf, 0xb0, 0x88, 0x2a, 0xba, 0xef, 0x56, 0xf7, 0xb4, 0xb7, 0x31, 0x32, 0x9b, + 0x83, 0x24, 0x9b, 0x52, 0xa6, 0x26, 0xaa, 0x55, 0x7b, 0x7d, 0x1d, 0xe8, 0x87, 0xb8, 0x59, 0xf8, + 0x82, 0xd3, 0x86, 0x37, 0x1c, 0x0b, 0x01, 0x54, 0x9a, 0xdb, 0x84, 0xe5, 0x0a, 0xeb, 0x5c, 0x29, + 0xa4, 0x17, 0xe9, 0x3d, 0xaf, 0x54, 0x0d, 0x2a, 0x24, 0xce, 0x12, 0x76, 0x54, 0x26, 0xf3, 0x00, + 0xa9, 0x51, 0x96, 0x7c, 0x11, 0x99, 0xfa, 0xe3, 0x82, 0x2c, 0x21, 0x86, 0xf2, 0xa1, 0x29, 0x47, + 0xd5, 0x94, 0x3d, 0x8d, 0xb4, 0x64, 0x74, 0x15, 0x64, 0xff, 0x69, 0x54, 0x2c, 0x90, 0x04, 0xbf, + 0x4d, 0x62, 0x4a, 0x0b, 0xa4, 0x23, 0xfb, 0x1e, 0xf4, 0x44, 0x1a, 0x4e, 0x43, 0x4a, 0x88, 0xa8, + 0x44, 0xfc, 0xe2, 0x47, 0x40, 0x11, 0x6c, 0xe1, 0xa7, 0x6c, 0x68, 0xab, 0xc3, 0x74, 0x4e, 0x8e, + 0x54, 0x63, 0x70, 0x33, 0x33, 0x99, 0x86, 0xbe, 0xc4, 0xe5, 0xa8, 0x1b, 0x2f, 0xaa, 0x94, 0x35, + 0x50, 0xe0, 0xfd, 0xa7, 0x11, 0xe5, 0x84, 0x6e, 0xc1, 0x92, 0x2f, 0xa2, 0x7c, 0x16, 0xd3, 0xca, + 0xdc, 0x88, 0xc7, 0x64, 0x45, 0x5a, 0xce, 0x40, 0x81, 0x71, 0x7d, 0x3b, 0x3c, 0xd6, 0x65, 0x33, + 0x2f, 0x8a, 0x50, 0x21, 0x09, 0x2f, 0xd0, 0x59, 0xd1, 0xbe, 0x01, 0xee, 0x08, 0x2f, 0x60, 0xdf, + 0x85, 0x6b, 0x88, 0x73, 0xf9, 0x2c, 0x91, 0xa7, 0x6e, 0x9c, 0xcf, 0x78, 0x1a, 0xfa, 0xae, 0x97, + 0xb9, 0x2f, 0x78, 0x2a, 0x74, 0xa2, 0xfd, 0x2a, 0x52, 0x3c, 0x40, 0x82, 0x47, 0x0a, 0xbf, 0x99, + 0x7d, 0xc6, 0x53, 0xc1, 0x3e, 0xa3, 0xbc, 0xd0, 0x79, 0x72, 0x6b, 0x2c, 0xc9, 0x5b, 0xe5, 0x5e, + 0x5d, 0x40, 0x49, 0x55, 0x08, 0x44, 0x38, 0x46, 0x60, 0x69, 0xbc, 0xed, 0x03, 0xa8, 0xdb, 0x3d, + 0x24, 0x59, 0x6f, 0x43, 0x47, 0x1e, 0x46, 0x94, 0x2e, 0xaf, 0x9d, 0x9b, 0x2e, 0x6f, 0xcb, 0x43, + 0xe4, 0x79, 0xe5, 0x8c, 0xd5, 0x49, 0x54, 0x75, 0x0f, 0x25, 0x38, 0x0a, 0x67, 0xa1, 0xd4, 0xb7, + 0x69, 0x54, 0xc7, 0x3e, 0x84, 0x2e, 0xcd, 0x40, 0xdf, 0x28, 0xea, 0xda, 0xb5, 0x97, 0xd7, 0xb5, + 0xdf, 0x83, 0xbe, 0xd6, 0x8b, 0x17, 0x15, 0xca, 0x7b, 0x0a, 0x8f, 0xed, 0xcc, 0x7e, 0x17, 0xba, + 0xff, 0xdb, 0x8b, 0x72, 0xf5, 0x8d, 0x37, 0xa1, 0x47, 0x15, 0x18, 0xf7, 0x30, 0x12, 0xfe, 0xb1, + 0xa9, 0x0c, 0x10, 0xe8, 0x1e, 0x42, 0x6c, 0x00, 0xeb, 0x49, 0x1c, 0x8a, 0x78, 0x33, 0x8a, 0xec, + 0xbf, 0x6d, 0x43, 0xf7, 0x13, 0x2f, 0x3b, 0x22, 0x35, 0x8a, 0x47, 0x98, 0xaa, 0xf6, 0x94, 0x5a, + 0x99, 0x79, 0x89, 0xae, 0xdc, 0xf7, 0x10, 0x88, 0x54, 0xbb, 0x5e, 0xb2, 0x90, 0x79, 0xa9, 0x2f, + 0x64, 0x5e, 0xde, 0x52, 0xd7, 0xbd, 0x54, 0x0d, 0x88, 0x9b, 0x52, 0x30, 0x4d, 0x70, 0x4f, 0x81, + 0xd8, 0xbb, 0xc0, 0x88, 0xc4, 0x8b, 0x22, 0x41, 0xee, 0x4e, 0xc6, 0xa3, 0x4c, 0x27, 0x69, 0x46, + 0x88, 0xd9, 0xd4, 0x88, 0x7d, 0xae, 0xce, 0x4f, 0xc5, 0x76, 0xb6, 0x16, 0x6d, 0xe7, 0x6d, 0x00, + 0xf4, 0x0a, 0x29, 0x2d, 0xb8, 0x10, 0x1c, 0xab, 0x0c, 0x49, 0x89, 0xfd, 0x12, 0x9e, 0xda, 0xdb, + 0x30, 0x2a, 0x28, 0x52, 0x3e, 0x71, 0xfd, 0x58, 0x6a, 0x77, 0x6d, 0xa0, 0xa9, 0x1c, 0x3e, 0xd9, + 0x8a, 0xe5, 0xa2, 0x4b, 0xd7, 0x3d, 0xe3, 0xd2, 0xfd, 0x04, 0x2e, 0x2d, 0x18, 0xb8, 0x2c, 0xe1, + 0xbe, 0x2e, 0x0e, 0x7f, 0x95, 0xfb, 0x48, 0xaf, 0x82, 0x45, 0xb9, 0xf6, 0x20, 0x4f, 0xf4, 0xd9, + 0xea, 0x84, 0x19, 0xb9, 0xde, 0x17, 0xb9, 0x8d, 0xfd, 0xff, 0x2e, 0xb7, 0x71, 0xf0, 0xe5, 0xdc, + 0xc6, 0xe1, 0x97, 0x73, 0x1b, 0x17, 0xdc, 0xac, 0xa5, 0xc5, 0xe8, 0xec, 0xc2, 0x58, 0x68, 0x74, + 0x61, 0x2c, 0xf4, 0x05, 0x81, 0xcc, 0xf2, 0x4b, 0x03, 0x99, 0x2f, 0x11, 0x49, 0xb1, 0x2f, 0x88, + 0xa4, 0xec, 0x27, 0x00, 0x64, 0x23, 0x69, 0xc9, 0x17, 0xed, 0x79, 0xed, 0xab, 0xee, 0xb9, 0xfd, + 0x1f, 0x35, 0x80, 0x7d, 0x6f, 0x96, 0x28, 0x57, 0x86, 0xfd, 0x18, 0x7a, 0x19, 0xf5, 0xaa, 0x89, + 0xac, 0x37, 0x2b, 0x37, 0x14, 0x0b, 0x52, 0xdd, 0xa4, 0xa4, 0x16, 0x64, 0x45, 0x9b, 0xc4, 0x55, + 0xcd, 0x50, 0x94, 0x98, 0x5a, 0x86, 0x80, 0x8c, 0xef, 0x4d, 0x18, 0x6a, 0x82, 0x84, 0xa7, 0x3e, + 0x8f, 0x95, 0x0e, 0xab, 0x39, 0x03, 0x05, 0xdd, 0x53, 0x40, 0xf6, 0x41, 0x41, 0xa6, 0xac, 0x40, + 0x76, 0x4e, 0x34, 0xa6, 0x87, 0x6c, 0x29, 0x02, 0x7b, 0xc3, 0xfc, 0x14, 0x5a, 0x88, 0x05, 0x4d, + 0xfc, 0xde, 0xe8, 0x15, 0xd6, 0x83, 0x8e, 0x9e, 0x75, 0x54, 0x63, 0x03, 0xe8, 0xd2, 0x5d, 0x2e, + 0xc2, 0xd5, 0xed, 0xff, 0xb7, 0x0c, 0xbd, 0xed, 0x38, 0x93, 0x69, 0xae, 0x44, 0xb3, 0xbc, 0xb2, + 0xd4, 0xa2, 0x2b, 0x4b, 0xba, 0x5a, 0xa9, 0x7e, 0x06, 0x55, 0x2b, 0xdf, 0x83, 0x8e, 0xbe, 0x1c, + 0xa7, 0xfd, 0xdb, 0x73, 0x6f, 0xd6, 0x19, 0x1a, 0xb6, 0x0e, 0x56, 0xa0, 0x6f, 0xed, 0xe9, 0x6c, + 0x5d, 0xe5, 0x2a, 0x9d, 0xb9, 0xcf, 0xe7, 0x14, 0x34, 0xec, 0x2d, 0x68, 0x78, 0xd3, 0x29, 0x69, + 0x1f, 0x2a, 0x61, 0x18, 0x52, 0x32, 0x26, 0x0e, 0xe2, 0xd8, 0x1d, 0xe8, 0x92, 0x5a, 0xa4, 0x84, + 0x75, 0x7b, 0x71, 0x4e, 0x93, 0x0d, 0x57, 0x9a, 0x92, 0x5c, 0xe3, 0x3b, 0xd0, 0x8d, 0x84, 0x48, + 0xd4, 0x80, 0xce, 0xe2, 0x00, 0x93, 0xc3, 0x74, 0xac, 0xc8, 0x64, 0x33, 0x6f, 0x41, 0x1b, 0xdd, + 0x14, 0x91, 0x68, 0xf3, 0x5e, 0x59, 0x07, 0xe5, 0xf2, 0x9c, 0x56, 0x86, 0x7f, 0xd8, 0x06, 0x80, + 0x92, 0x6b, 0x9a, 0xb9, 0xbb, 0xc8, 0x8e, 0x22, 0x6c, 0xc7, 0xc3, 0x67, 0x22, 0xf8, 0x7b, 0x30, + 0x52, 0x21, 0x5a, 0x65, 0x24, 0x98, 0xea, 0x9c, 0x19, 0x39, 0x1f, 0xf5, 0x3b, 0xc3, 0x74, 0x3e, + 0x0b, 0xf0, 0x0e, 0x74, 0x12, 0x15, 0xa3, 0x90, 0xe6, 0xe8, 0x6d, 0x2c, 0x97, 0x43, 0x75, 0xf0, + 0xe2, 0x18, 0x0a, 0xf6, 0x43, 0x18, 0xaa, 0x2a, 0xd2, 0x44, 0x3b, 0xeb, 0x94, 0x1f, 0x9e, 0xbb, + 0x94, 0x35, 0xe7, 0xcb, 0x3b, 0x03, 0x39, 0xe7, 0xda, 0x7f, 0x0f, 0x06, 0xe5, 0x25, 0x19, 0xdf, + 0x8b, 0x49, 0x9f, 0xf4, 0x36, 0xae, 0x96, 0xc3, 0xab, 0x5e, 0xa3, 0xd3, 0xe7, 0x55, 0x1f, 0x72, + 0x0d, 0xda, 0xba, 0xb2, 0x39, 0xa2, 0x51, 0x95, 0x4b, 0xd4, 0xaa, 0x96, 0xe1, 0x68, 0x3c, 0xf2, + 0xb2, 0x2c, 0xda, 0x8c, 0xd9, 0x22, 0x2f, 0x8b, 0x8a, 0x8d, 0xd3, 0x2d, 0x8a, 0x35, 0xec, 0xc1, + 0x7c, 0x11, 0x49, 0x15, 0x4b, 0x2e, 0xd1, 0xd0, 0x57, 0xcf, 0x19, 0xaa, 0x6a, 0x26, 0xce, 0x52, + 0xb2, 0x50, 0x8b, 0x7a, 0x17, 0x2c, 0x91, 0x06, 0x54, 0xc5, 0xa6, 0x94, 0x0e, 0xf1, 0x93, 0x6a, + 0x67, 0xea, 0x46, 0x20, 0x29, 0x8f, 0x8e, 0x50, 0x1d, 0x74, 0x18, 0x92, 0x54, 0x7c, 0xce, 0x7d, + 0xa9, 0x54, 0xd7, 0x95, 0xb3, 0x0e, 0x83, 0xc6, 0x93, 0x67, 0x79, 0x03, 0x3a, 0xa6, 0x5e, 0x7b, + 0xf5, 0x0c, 0xa5, 0x41, 0xb1, 0x0f, 0x61, 0x69, 0x5e, 0xa1, 0x65, 0xe3, 0x95, 0x33, 0xd4, 0xc3, + 0x39, 0xfd, 0x85, 0x56, 0x56, 0x7b, 0x41, 0xe3, 0x33, 0x4e, 0xa8, 0x42, 0xa0, 0x9f, 0xaa, 0xfd, + 0xa7, 0x57, 0xcf, 0xfa, 0xa9, 0xda, 0x97, 0x1a, 0x43, 0x27, 0xcc, 0x1e, 0x86, 0x69, 0x26, 0xc7, + 0xd7, 0x8c, 0xd5, 0xa3, 0x2e, 0x7a, 0x5f, 0x61, 0x86, 0xea, 0x7f, 0x7c, 0xdd, 0xdc, 0x21, 0x25, + 0x63, 0x70, 0x1b, 0xda, 0xba, 0x96, 0xbd, 0x7a, 0xe6, 0x44, 0xeb, 0xfb, 0x1f, 0x8e, 0xa6, 0x60, + 0xdf, 0x84, 0x0e, 0x15, 0x32, 0x45, 0x32, 0x7e, 0x6b, 0x51, 0x02, 0x54, 0x35, 0xd1, 0x69, 0x47, + 0xaa, 0xaa, 0xf8, 0x0e, 0x74, 0x8c, 0xf3, 0x61, 0x2f, 0x4a, 0xb5, 0x76, 0x42, 0x1c, 0x43, 0xc1, + 0x6e, 0x42, 0x6b, 0x86, 0x7a, 0x6c, 0xfc, 0x8d, 0xc5, 0x13, 0xaa, 0xd4, 0x9b, 0xc2, 0xb2, 0xbb, + 0xd0, 0xcb, 0xc8, 0xef, 0x54, 0xa2, 0x7b, 0xc3, 0x14, 0x01, 0xe7, 0xaf, 0x9c, 0x93, 0xe0, 0x42, + 0x56, 0x3a, 0xa8, 0xff, 0x07, 0xae, 0x55, 0x2b, 0x88, 0xa6, 0xbc, 0xa8, 0xe3, 0xb6, 0x9b, 0x34, + 0xcb, 0x5b, 0xe7, 0x48, 0xd8, 0x7c, 0x21, 0xd2, 0x59, 0x49, 0x2e, 0xa8, 0x50, 0xde, 0x2d, 0xac, + 0x04, 0x1e, 0xca, 0xf1, 0xad, 0x33, 0xcb, 0x2a, 0xec, 0x8c, 0xb1, 0x1d, 0x64, 0x9e, 0x3e, 0x86, + 0xfe, 0x24, 0x7f, 0xf1, 0xe2, 0x54, 0xcb, 0xc8, 0xf8, 0x6d, 0x1a, 0x57, 0x89, 0xa0, 0x2a, 0x45, + 0x4b, 0xa7, 0x37, 0xa9, 0x54, 0x30, 0x57, 0xa0, 0xe3, 0xc7, 0xae, 0x17, 0x04, 0xe9, 0x78, 0x4d, + 0x15, 0x2d, 0xfd, 0x78, 0x33, 0x08, 0xe8, 0xf6, 0xbc, 0x48, 0x38, 0x5d, 0x6b, 0x75, 0xc3, 0x60, + 0xfc, 0x4d, 0x65, 0xaf, 0x0c, 0x68, 0x3b, 0xa0, 0xeb, 0xf5, 0x26, 0xec, 0x08, 0x83, 0xf1, 0x6d, + 0x7d, 0xbd, 0x5e, 0x83, 0xb6, 0x03, 0xf4, 0x43, 0x67, 0xde, 0x89, 0x6b, 0x20, 0xe3, 0x77, 0x54, + 0x2c, 0x3a, 0xf3, 0x4e, 0xf6, 0x34, 0x08, 0xcf, 0xb6, 0xba, 0xb2, 0x44, 0xda, 0xee, 0xdd, 0xc5, + 0xb3, 0x5d, 0x24, 0x31, 0x9c, 0x6e, 0x58, 0xe4, 0x33, 0x48, 0x1f, 0x90, 0x06, 0x73, 0xa3, 0x8d, + 0xf1, 0x7b, 0x67, 0xf5, 0x81, 0xce, 0xd1, 0xa0, 0x3e, 0x30, 0xe9, 0x9a, 0x0d, 0x00, 0xa5, 0xea, + 0x68, 0xb3, 0xd7, 0x17, 0xc7, 0x14, 0xc1, 0x81, 0xa3, 0xee, 0xeb, 0xd0, 0x56, 0x6f, 0x00, 0xd0, + 0xc5, 0x1f, 0x35, 0xe6, 0xce, 0xe2, 0x98, 0xc2, 0xd9, 0x77, 0xba, 0xcf, 0x0a, 0xbf, 0xff, 0x0e, + 0x74, 0x73, 0x74, 0xeb, 0xd1, 0xb1, 0x1e, 0xbf, 0xbf, 0x78, 0x06, 0x8c, 0xc7, 0xef, 0x58, 0xb9, + 0x6e, 0xe1, 0x47, 0xc8, 0x64, 0x91, 0xf7, 0x32, 0xfe, 0x60, 0xf1, 0x23, 0x45, 0x58, 0xe0, 0x90, + 0x65, 0x53, 0x11, 0xc2, 0x5d, 0xe8, 0x29, 0xa6, 0xa9, 0x41, 0x1b, 0x8b, 0x32, 0x52, 0xba, 0x43, + 0x8e, 0xe2, 0xae, 0x1a, 0x76, 0x13, 0x5a, 0x5e, 0x92, 0x44, 0xa7, 0xe3, 0x0f, 0x17, 0x0f, 0xc6, + 0x26, 0x82, 0x1d, 0x85, 0x45, 0x51, 0x9a, 0xe5, 0x91, 0x0c, 0xcd, 0x15, 0x9b, 0x8f, 0x16, 0x45, + 0xa9, 0x72, 0x03, 0xd1, 0xe9, 0xcd, 0x2a, 0xd7, 0x11, 0xdf, 0x05, 0x2b, 0x11, 0x99, 0x74, 0x83, + 0x59, 0x34, 0xbe, 0x7b, 0xc6, 0xfa, 0xa8, 0xab, 0x25, 0x4e, 0x27, 0xd1, 0x77, 0x73, 0xe6, 0xee, + 0xc5, 0x7e, 0x6b, 0xfe, 0x5e, 0xec, 0x4f, 0x9b, 0xd6, 0xf2, 0x88, 0xd9, 0x77, 0xa1, 0xbf, 0x49, + 0xcf, 0x5c, 0xc2, 0x8c, 0x34, 0xe6, 0x4d, 0x68, 0x16, 0x09, 0xb7, 0x42, 0x15, 0x13, 0xc5, 0x0b, + 0xbe, 0x1d, 0x4f, 0x84, 0x43, 0x68, 0xfb, 0x6f, 0x9a, 0xd0, 0xde, 0x17, 0x79, 0xea, 0xf3, 0x2f, + 0xbe, 0xa1, 0xf5, 0xba, 0x11, 0x8c, 0xb8, 0x2c, 0xdb, 0x2b, 0x19, 0x20, 0xf4, 0x62, 0xc1, 0xb1, + 0x5b, 0xe6, 0xf2, 0x2e, 0x43, 0x4b, 0x05, 0x77, 0xea, 0x66, 0x8f, 0xea, 0xd0, 0xa1, 0xc8, 0xb3, + 0x23, 0x7a, 0xcb, 0x12, 0xaa, 0x4b, 0xe0, 0x4d, 0x07, 0x0c, 0x68, 0x3b, 0xa0, 0x60, 0xdd, 0x10, + 0xd0, 0xa9, 0x6b, 0x2b, 0x0f, 0xdf, 0x00, 0xe9, 0xec, 0x99, 0x3c, 0x61, 0xe7, 0x82, 0x3c, 0xe1, + 0x1b, 0xd0, 0x8c, 0xcd, 0x8d, 0x92, 0x02, 0x4f, 0x4f, 0x0f, 0x08, 0xce, 0x6e, 0x43, 0x71, 0xad, + 0x4c, 0x3b, 0x1f, 0x17, 0x5f, 0x3b, 0xdb, 0x80, 0x6e, 0xf1, 0x30, 0x4a, 0xfb, 0x1b, 0x97, 0xd7, + 0xcb, 0xa7, 0x52, 0x07, 0xa6, 0xe5, 0x94, 0x64, 0xe7, 0xa4, 0x0e, 0x55, 0xd5, 0x85, 0xf8, 0xd4, + 0xfb, 0x2a, 0xa9, 0x43, 0x2a, 0xc5, 0x98, 0xb4, 0x69, 0x98, 0xb9, 0xbe, 0x88, 0x33, 0xa9, 0xd3, + 0x11, 0x9d, 0x30, 0xdb, 0xc2, 0x2e, 0xfb, 0x0e, 0x0c, 0x52, 0xee, 0x3f, 0x73, 0x67, 0xd9, 0x54, + 0x7d, 0x62, 0x50, 0xbd, 0xa8, 0x3a, 0xcb, 0xa6, 0x9f, 0x70, 0x0f, 0x4d, 0xb0, 0x8a, 0x79, 0x7a, + 0x48, 0xbb, 0x9b, 0x4d, 0x69, 0xd6, 0x77, 0x60, 0x79, 0xc6, 0x67, 0x87, 0x3c, 0xcd, 0x8e, 0xc2, + 0xc4, 0x68, 0xc7, 0x21, 0x65, 0x0c, 0x47, 0x25, 0x42, 0xad, 0xc5, 0xfe, 0xfd, 0x1a, 0x58, 0xc8, + 0x45, 0x94, 0x25, 0xc6, 0xa0, 0x39, 0xf3, 0x93, 0x5c, 0xbb, 0xbc, 0xd4, 0xd6, 0x8f, 0xad, 0x94, + 0x94, 0xe8, 0xc7, 0x56, 0xb4, 0x87, 0x0d, 0x95, 0x22, 0xc4, 0xb6, 0x7a, 0xee, 0x70, 0x4a, 0x79, + 0x18, 0x25, 0x19, 0xa6, 0xcb, 0xae, 0x40, 0xdb, 0x8f, 0x29, 0x9e, 0x55, 0xf7, 0x8c, 0x5a, 0x7e, + 0x8c, 0x71, 0xac, 0x02, 0x97, 0xd7, 0x1b, 0x5a, 0x7e, 0xbc, 0x1d, 0x9c, 0xd8, 0x7f, 0x51, 0x83, + 0xe5, 0xbd, 0x54, 0xf8, 0x3c, 0xcb, 0x76, 0xd0, 0x64, 0x7b, 0xe4, 0x73, 0x31, 0x68, 0x52, 0x1e, + 0x4d, 0xbd, 0x1d, 0xa0, 0x36, 0xca, 0xb0, 0x4a, 0x36, 0x14, 0x81, 0x45, 0xc3, 0xe9, 0x12, 0x84, + 0xe2, 0x8a, 0x02, 0x4d, 0x03, 0x1b, 0x15, 0x34, 0x65, 0xe0, 0x6e, 0xc2, 0xb0, 0xbc, 0xf4, 0x53, + 0x49, 0x0a, 0x96, 0x57, 0xb1, 0x69, 0x96, 0x37, 0xa1, 0x97, 0x12, 0x97, 0xd5, 0x34, 0x2a, 0x41, + 0x08, 0x0a, 0x84, 0xf3, 0xd8, 0x47, 0x30, 0xda, 0x4b, 0x79, 0xe2, 0xa5, 0x1c, 0xb5, 0xfb, 0x8c, + 0x78, 0x78, 0x15, 0xda, 0x11, 0x8f, 0xa7, 0xf2, 0x48, 0xaf, 0x57, 0xf7, 0x8a, 0x87, 0x70, 0xf5, + 0xca, 0x43, 0x38, 0xe4, 0x65, 0xca, 0x3d, 0xfd, 0x5e, 0x8e, 0xda, 0x78, 0xc6, 0xe2, 0x3c, 0xd2, + 0xb9, 0x3d, 0xcb, 0x51, 0x1d, 0xfb, 0x17, 0x0d, 0xe8, 0x69, 0xce, 0xd0, 0x57, 0xd4, 0xae, 0xd4, + 0x8a, 0x5d, 0x19, 0x41, 0x23, 0x7b, 0x1a, 0xe9, 0x6d, 0xc2, 0x26, 0xfb, 0x10, 0x1a, 0x51, 0x38, + 0xd3, 0x61, 0xc9, 0xf5, 0x39, 0x5b, 0x31, 0xcf, 0x5f, 0x2d, 0x42, 0x48, 0x8d, 0x0a, 0x8a, 0x6e, + 0x7d, 0xa3, 0xb0, 0x6a, 0x9e, 0xa0, 0xde, 0x3e, 0xc1, 0x13, 0x81, 0x4c, 0xf5, 0x7c, 0xba, 0x37, + 0x69, 0x8e, 0xf9, 0xc0, 0xe9, 0x6a, 0xc8, 0x76, 0xc0, 0x3e, 0x02, 0x2b, 0x8b, 0xbd, 0x24, 0x3b, + 0x12, 0xb2, 0x08, 0x44, 0xe4, 0x49, 0xbc, 0xbe, 0xf5, 0xe8, 0xe0, 0x24, 0xde, 0xd7, 0x18, 0xfd, + 0xb1, 0x82, 0x92, 0xfd, 0x10, 0xfa, 0x19, 0xcf, 0x32, 0x75, 0x91, 0x77, 0x22, 0xf4, 0xf1, 0xbf, + 0x52, 0x8d, 0x31, 0x08, 0x8b, 0xbf, 0xda, 0x08, 0x7b, 0x56, 0x82, 0xd8, 0x27, 0x30, 0x34, 0xe3, + 0x23, 0x31, 0x9d, 0x16, 0x49, 0xc8, 0xeb, 0x67, 0x66, 0xd8, 0x21, 0x74, 0x65, 0x9e, 0x41, 0x56, + 0x45, 0xb0, 0x9f, 0xc0, 0x30, 0x51, 0x9b, 0xe9, 0xea, 0x2c, 0xbb, 0x52, 0x23, 0xd7, 0xe6, 0x5c, + 0x9b, 0xb9, 0xcd, 0x2e, 0x2f, 0xe7, 0x95, 0xf0, 0xcc, 0xfe, 0xa3, 0x3a, 0xf4, 0x2a, 0xab, 0xa6, + 0xe7, 0x89, 0x19, 0x4f, 0x4d, 0x52, 0x1d, 0xdb, 0x08, 0x3b, 0x12, 0xfa, 0xf5, 0x4b, 0xd7, 0xa1, + 0x36, 0xc2, 0x52, 0xa1, 0x0b, 0x35, 0x5d, 0x87, 0xda, 0xa8, 0x3a, 0x75, 0xf0, 0xa8, 0xde, 0x07, + 0xd0, 0xa6, 0x34, 0x9d, 0x7e, 0x09, 0xdc, 0x0e, 0xe8, 0x1d, 0xa3, 0x27, 0xbd, 0x43, 0x2f, 0x33, + 0x35, 0x80, 0xa2, 0x8f, 0x47, 0xf3, 0x19, 0x4f, 0x71, 0x2d, 0x5a, 0xeb, 0x9a, 0x2e, 0xee, 0x35, + 0x69, 0xb3, 0x17, 0x22, 0x56, 0x4f, 0x48, 0xfa, 0x8e, 0x85, 0x80, 0xcf, 0x44, 0x4c, 0xc3, 0xf4, + 0xce, 0x12, 0x3f, 0xbb, 0x8e, 0xe9, 0xa2, 0xce, 0x7a, 0x9a, 0x73, 0x74, 0xff, 0x02, 0xba, 0x45, + 0xd4, 0x75, 0x3a, 0xd4, 0xdf, 0x0e, 0xd8, 0x6d, 0xa0, 0xab, 0x78, 0xee, 0x73, 0x2f, 0x94, 0x24, + 0x42, 0x22, 0x97, 0xa4, 0x5e, 0x1b, 0xce, 0x12, 0x22, 0x3e, 0xf5, 0x42, 0x79, 0xa0, 0xc0, 0xf6, + 0xbf, 0xd6, 0x60, 0xf9, 0xcc, 0xc6, 0xa0, 0x67, 0x86, 0x9b, 0x62, 0xee, 0x57, 0xf6, 0x9d, 0x36, + 0x76, 0xb7, 0x03, 0x42, 0xc8, 0x19, 0x09, 0x5e, 0x5d, 0x23, 0xe4, 0x0c, 0xa5, 0xee, 0x0a, 0xb4, + 0xe5, 0x09, 0x71, 0x46, 0x1d, 0xa2, 0x96, 0x3c, 0x41, 0x96, 0x6c, 0x62, 0x94, 0x3b, 0x75, 0x23, + 0xfe, 0x8c, 0x47, 0xc4, 0xb3, 0xe1, 0xc6, 0x8d, 0x97, 0x48, 0xc4, 0xfa, 0x8e, 0x98, 0xee, 0x20, + 0x2d, 0xc6, 0xbd, 0xaa, 0x65, 0xff, 0x14, 0x2c, 0x03, 0x65, 0x5d, 0x68, 0xdd, 0xe7, 0x87, 0xf9, + 0x74, 0xf4, 0x0a, 0xb3, 0xa0, 0x89, 0x23, 0x46, 0x35, 0x6c, 0x7d, 0xea, 0xa5, 0xf1, 0xa8, 0x8e, + 0xe8, 0x07, 0x69, 0x2a, 0xd2, 0x51, 0x03, 0x9b, 0x7b, 0x5e, 0x1c, 0xfa, 0xa3, 0x26, 0x36, 0x1f, + 0x7a, 0xd2, 0x8b, 0x46, 0x2d, 0xfb, 0x97, 0x2d, 0xb0, 0xf6, 0xf4, 0xd7, 0xd9, 0x7d, 0x18, 0x14, + 0x6f, 0x34, 0xcf, 0xcf, 0xac, 0xec, 0x2d, 0x36, 0x28, 0xb3, 0xd2, 0x4f, 0x2a, 0xbd, 0xc5, 0x97, + 0x9e, 0xf5, 0x33, 0x2f, 0x3d, 0x5f, 0x83, 0xc6, 0xd3, 0xf4, 0x74, 0xbe, 0xee, 0xb6, 0x17, 0x79, + 0xb1, 0x83, 0x60, 0xf6, 0x01, 0xf4, 0x50, 0x46, 0xdc, 0x8c, 0x9c, 0x06, 0x9d, 0x8d, 0xa8, 0xbe, + 0xfc, 0x25, 0xb8, 0x03, 0x48, 0xa4, 0x1d, 0x8b, 0x75, 0xb0, 0xfc, 0xa3, 0x30, 0x0a, 0x52, 0x1e, + 0xeb, 0x9a, 0x36, 0x3b, 0xbb, 0x64, 0xa7, 0xa0, 0x61, 0x3f, 0x86, 0x51, 0x58, 0x66, 0x53, 0xca, + 0x52, 0xc3, 0xdc, 0xf1, 0xae, 0xe4, 0x5b, 0x9c, 0xa5, 0x0a, 0x39, 0x59, 0xb2, 0xf2, 0xe2, 0x7b, + 0xa7, 0x7a, 0xf1, 0x5d, 0xbd, 0xe7, 0x23, 0x73, 0x63, 0x15, 0xb1, 0x18, 0x5a, 0x9b, 0x5b, 0xda, + 0x47, 0xe8, 0x2e, 0x7a, 0xa1, 0xc6, 0xc2, 0x69, 0x5f, 0xe1, 0x06, 0x0c, 0xd1, 0xf7, 0x70, 0x95, + 0xcb, 0x82, 0x6a, 0x07, 0xf4, 0xab, 0x9b, 0x3c, 0x3b, 0xba, 0x8f, 0x4e, 0x0b, 0x0a, 0xe3, 0x4d, + 0x18, 0x9a, 0xdf, 0xa2, 0xaf, 0x8b, 0xf7, 0x74, 0x29, 0x42, 0x43, 0xd5, 0x55, 0xf1, 0x75, 0xb8, + 0xe4, 0x1f, 0x79, 0x71, 0xcc, 0x23, 0xf7, 0x30, 0x9f, 0x4c, 0x8c, 0xb5, 0xe8, 0x53, 0x12, 0x6f, + 0x59, 0xa3, 0xee, 0x11, 0x86, 0x8c, 0x8f, 0x0d, 0x83, 0x38, 0x8c, 0x54, 0xa6, 0x9a, 0x2c, 0xe3, + 0x80, 0x28, 0x7b, 0x71, 0x18, 0x51, 0xaa, 0x1a, 0xed, 0xe3, 0x8f, 0x60, 0x94, 0xe7, 0x61, 0x90, + 0xb9, 0x52, 0x98, 0xa7, 0x90, 0x3a, 0xdf, 0x59, 0xc9, 0x34, 0x3c, 0xc9, 0xc3, 0xe0, 0x40, 0xe8, + 0xc7, 0x90, 0x03, 0xa2, 0x37, 0x5d, 0xfb, 0x47, 0xd0, 0xaf, 0xca, 0x0e, 0xca, 0x22, 0x85, 0x82, + 0xa3, 0x57, 0x18, 0x40, 0xfb, 0x91, 0x48, 0x67, 0x5e, 0x34, 0xaa, 0x61, 0x5b, 0x3d, 0x07, 0x19, + 0xd5, 0x59, 0x1f, 0x2c, 0x13, 0xa3, 0x8c, 0x1a, 0xf6, 0xf7, 0xc0, 0x32, 0x6f, 0x3b, 0xe9, 0x51, + 0x9d, 0x08, 0xb8, 0xf2, 0xdd, 0x94, 0x16, 0xb3, 0x10, 0x40, 0x7e, 0x9b, 0x79, 0x7c, 0x5d, 0x2f, + 0x1f, 0x5f, 0xdb, 0xff, 0x0b, 0xfa, 0xd5, 0xc5, 0x99, 0xc4, 0x59, 0xad, 0x4c, 0x9c, 0x9d, 0x33, + 0x8a, 0x6a, 0x52, 0xa9, 0x98, 0xb9, 0x15, 0xf7, 0xc2, 0x42, 0x00, 0x7e, 0xc6, 0xfe, 0xbd, 0x1a, + 0xb4, 0xc8, 0x67, 0x27, 0x33, 0x84, 0x8d, 0xf2, 0xec, 0xb4, 0x9c, 0x2e, 0x41, 0xfe, 0x0b, 0x57, + 0xe9, 0x8a, 0x02, 0x49, 0xf3, 0xa5, 0x05, 0x92, 0xdb, 0x7f, 0x5a, 0x83, 0xb6, 0x7a, 0xf0, 0xce, + 0x96, 0x61, 0xf0, 0x24, 0x3e, 0x8e, 0xc5, 0xf3, 0x58, 0x01, 0x46, 0xaf, 0xb0, 0x4b, 0xb0, 0x64, + 0xb8, 0xae, 0x5f, 0xd6, 0x8f, 0x6a, 0x6c, 0x04, 0x7d, 0xda, 0x57, 0x03, 0xa9, 0xb3, 0xd7, 0x60, + 0xac, 0x2d, 0xc9, 0x7d, 0x11, 0xf3, 0x47, 0x42, 0x86, 0x93, 0x53, 0x83, 0x6d, 0xb0, 0x25, 0xe8, + 0xed, 0x4b, 0x91, 0xec, 0xf3, 0x38, 0x08, 0xe3, 0xe9, 0xa8, 0xc9, 0xc6, 0x70, 0xd9, 0xcc, 0xaa, + 0x22, 0xf4, 0x87, 0x61, 0x1c, 0x66, 0x47, 0xa3, 0x16, 0xbb, 0x0e, 0x2b, 0xe7, 0x61, 0x36, 0xfd, + 0xe3, 0x51, 0xfb, 0xf6, 0x47, 0xc0, 0xce, 0xbe, 0x21, 0xc7, 0xd9, 0x77, 0xf8, 0xd4, 0xf3, 0x4f, + 0xb7, 0x22, 0x91, 0xa1, 0x38, 0x0c, 0xa0, 0x5b, 0x8e, 0xaa, 0xdd, 0x7e, 0x08, 0x6d, 0xf5, 0xe8, + 0xbf, 0xf2, 0xfb, 0x14, 0x60, 0xf4, 0x0a, 0x0e, 0x46, 0x1d, 0x1e, 0xc6, 0xd3, 0x47, 0xfc, 0x44, + 0x2a, 0x15, 0xb8, 0xe3, 0x65, 0x72, 0x54, 0x67, 0x43, 0x00, 0xfd, 0x13, 0x1e, 0xc4, 0xc1, 0xa8, + 0x71, 0x6f, 0xeb, 0x57, 0xbf, 0x7e, 0xa3, 0xf6, 0xf7, 0xbf, 0x7e, 0xa3, 0xf6, 0xcf, 0xbf, 0x7e, + 0xe3, 0x95, 0x9f, 0xff, 0xcb, 0x1b, 0xb5, 0xcf, 0x3e, 0xa8, 0xfc, 0x4b, 0x83, 0x99, 0x27, 0xd3, + 0xf0, 0x44, 0xd5, 0x10, 0x4d, 0x27, 0xe6, 0x77, 0x92, 0xe3, 0xe9, 0x9d, 0xe4, 0xf0, 0x8e, 0x91, + 0xf0, 0xc3, 0x36, 0xfd, 0xa7, 0x82, 0x0f, 0xff, 0x33, 0x00, 0x00, 0xff, 0xff, 0xa1, 0x2a, 0x06, + 0x1a, 0x28, 0x41, 0x00, 0x00, } func (m *Message) Marshal() (dAtA []byte, err error) { @@ -5749,6 +5807,16 @@ func (m *Message) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if m.AcceptedTeardownMode != 0 { + i = encodeVarintPipeline(dAtA, i, uint64(m.AcceptedTeardownMode)) + i-- + dAtA[i] = 0x60 + } + if m.RequestedTeardownMode != 0 { + i = encodeVarintPipeline(dAtA, i, uint64(m.RequestedTeardownMode)) + i-- + dAtA[i] = 0x58 + } if len(m.DebugMsg) > 0 { i -= len(m.DebugMsg) copy(dAtA[i:], m.DebugMsg) @@ -11054,6 +11122,12 @@ func (m *Message) ProtoSize() (n int) { if l > 0 { n += 1 + l + sovPipeline(uint64(l)) } + if m.RequestedTeardownMode != 0 { + n += 1 + sovPipeline(uint64(m.RequestedTeardownMode)) + } + if m.AcceptedTeardownMode != 0 { + n += 1 + sovPipeline(uint64(m.AcceptedTeardownMode)) + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -13529,6 +13603,44 @@ func (m *Message) Unmarshal(dAtA []byte) error { } m.DebugMsg = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 11: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RequestedTeardownMode", wireType) + } + m.RequestedTeardownMode = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPipeline + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RequestedTeardownMode |= StreamTeardownMode(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 12: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field AcceptedTeardownMode", wireType) + } + m.AcceptedTeardownMode = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPipeline + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.AcceptedTeardownMode |= StreamTeardownMode(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipPipeline(dAtA[iNdEx:]) diff --git a/pkg/sql/compile/remoterunClient.go b/pkg/sql/compile/remoterunClient.go index 10cdf38b2f548..7c091c17d5d7e 100644 --- a/pkg/sql/compile/remoterunClient.go +++ b/pkg/sql/compile/remoterunClient.go @@ -45,6 +45,8 @@ import ( // this is just a number I casually wrote, the purpose of doing this is that any message sent through rpc need a clear deadline. const MaxRpcTime = time.Hour * 24 +var pipelineStreamFinishClientTimeout = 30 * time.Second + // remoteRun sends a scope to remote node for running. // and keep receiving the back results. // @@ -104,8 +106,6 @@ func (s *Scope) remoteRun(c *Compile) (sender *messageSenderOnClient, err error) return sender, err } - sender.safeToClose = false - sender.alreadyClose = false err = receiveMessageFromCnServer(s, withoutOutput, sender) return sender, err } @@ -371,7 +371,18 @@ type messageSenderOnClient struct { // 2. we have never sent a message in succeed. safeToClose bool // alreadyClose should be true once we get a stream closed signal. - alreadyClose bool + alreadyClose bool + reuseEligible bool + terminalNegotiated bool + expectedEnd pipeline.Method + stateMu sync.Mutex + closeOnce sync.Once + requestFinishAck bool + // allowCleanupCancellation is set after successful local cleanup. Pipeline + // and query contexts may be intentionally cancelled by that cleanup; FIN + // then runs on its own bounded context. Cancellation before this transition + // still poisons reuse. + allowCleanupCancellation bool // gaugeDecOnce ensures PipelineMessageSenderGauge.Dec() is called at most once when close() runs // (including when close() returns early because alreadyClose is true, so the gauge still decrements). @@ -428,6 +439,7 @@ func newMessageSenderOnClient( mp: mp, anal: analyzeModule, streamSender: streamSender, + requestFinishAck: pipelineStreamReuseEnabled(sid), } if sender.receiveCh == nil { @@ -446,6 +458,19 @@ func newMessageSenderOnClient( return sender, nil } +func pipelineStreamReuseEnabled(serviceID string) bool { + runtime := moruntime.ServiceRuntime(serviceID) + if runtime == nil { + return true + } + value, ok := runtime.GetGlobalVariables(moruntime.EnablePipelineStreamReuse) + if !ok { + return true + } + enabled, ok := value.(bool) + return ok && enabled +} + func (sender *messageSenderOnClient) sendPipeline( scopeData, procData []byte, noDataBack bool, eachMessageSizeLimitation int, debugMsg string) error { sdLen := len(scopeData) @@ -457,8 +482,15 @@ func (sender *messageSenderOnClient) sendPipeline( message.SetData(scopeData) message.SetProcData(procData) message.SetSid(pipeline.Status_Last) + if sender.requestFinishAck { + message.RequestedTeardownMode = pipeline.StreamTeardownMode_FinishAck + } message.NeedNotReply = noDataBack - return sender.streamSender.Send(sender.ctx, message) + if err := sender.streamSender.Send(sender.ctx, message); err != nil { + return err + } + sender.markStreamActive(pipeline.Method_PipelineMessage) + return nil } start := 0 @@ -478,15 +510,58 @@ func (sender *messageSenderOnClient) sendPipeline( message.SetSid(pipeline.Status_WaitingNext) } message.NeedNotReply = noDataBack + if sender.requestFinishAck { + message.RequestedTeardownMode = pipeline.StreamTeardownMode_FinishAck + } if err := sender.streamSender.Send(sender.ctx, message); err != nil { return err } start = end } + sender.markStreamActive(pipeline.Method_PipelineMessage) return nil } +func (sender *messageSenderOnClient) markStreamActive(method pipeline.Method) { + sender.stateMu.Lock() + defer sender.stateMu.Unlock() + sender.safeToClose = false + sender.alreadyClose = false + sender.reuseEligible = false + sender.terminalNegotiated = false + sender.allowCleanupCancellation = false + sender.expectedEnd = method +} + +func (sender *messageSenderOnClient) markStreamClosed() { + sender.stateMu.Lock() + defer sender.stateMu.Unlock() + sender.safeToClose = true + sender.alreadyClose = true + sender.reuseEligible = false + sender.terminalNegotiated = false +} + +func (sender *messageSenderOnClient) markTerminal(message *pipeline.Message, successful bool) { + sender.stateMu.Lock() + defer sender.stateMu.Unlock() + sender.safeToClose = true + sender.terminalNegotiated = message.GetCmd() == sender.expectedEnd && + message.GetAcceptedTeardownMode() == pipeline.StreamTeardownMode_FinishAck + sender.reuseEligible = sender.terminalNegotiated && + (successful || sender.allowCleanupCancellation) +} + +func (sender *messageSenderOnClient) prepareForLocalCleanup() { + sender.stateMu.Lock() + defer sender.stateMu.Unlock() + sender.allowCleanupCancellation = true + if sender.terminalNegotiated { + sender.reuseEligible = true + } +} + func (sender *messageSenderOnClient) receiveMessage() (morpc.Message, error) { select { case <-sender.ctx.Done(): @@ -494,8 +569,7 @@ func (sender *messageSenderOnClient) receiveMessage() (morpc.Message, error) { case val, ok := <-sender.receiveCh: if !ok || val == nil { - sender.safeToClose = true - sender.alreadyClose = true + sender.markStreamClosed() return nil, moerr.NewStreamClosed(sender.ctx) } return val, nil @@ -521,17 +595,18 @@ func (sender *messageSenderOnClient) receiveBatch() (bat *batch.Batch, over bool m = val.(*pipeline.Message) if info, get := m.TryToGetMoErr(); get { - sender.safeToClose = true + sender.markTerminal(m, false) return nil, false, info } if m.IsEndMessage() { - sender.safeToClose = true + sender.markTerminal(m, true) anaData := m.GetAnalyse() if len(anaData) > 0 { var p models.PhyPlan err = json.Unmarshal(anaData, &p) if err != nil { + sender.markTerminal(m, false) return nil, false, err } @@ -606,7 +681,10 @@ func forwardRemoteBatchWithContext( // no matter how we stop the remote-run, we should get the final remote cost here. func (sender *messageSenderOnClient) waitingTheStopResponse() { - if sender.alreadyClose || sender.safeToClose { + sender.stateMu.Lock() + alreadyClose, safeToClose := sender.alreadyClose, sender.safeToClose + sender.stateMu.Unlock() + if alreadyClose || safeToClose { return } @@ -626,15 +704,18 @@ func (sender *messageSenderOnClient) waitingTheStopResponse() { select { case val, ok := <-sender.receiveCh: if !ok || val == nil { - sender.safeToClose = true - sender.alreadyClose = true + sender.markStreamClosed() return } message := val.(*pipeline.Message) if message.IsEndMessage() || len(message.GetErr()) > 0 { - sender.safeToClose = true + // StopSending is also a clean teardown when the original server + // worker answers with its negotiated terminal response. The later FIN + // still waits for the same server cleanup barrier. Unnegotiated or + // mismatched terminal responses remain poisoned. + sender.markTerminal(message, message.IsEndMessage()) // in fact, we should deal the cost analysis information here. return } @@ -645,6 +726,54 @@ func (sender *messageSenderOnClient) waitingTheStopResponse() { } } +func generatePipelineStreamFinishMessage(streamID uint64) *pipeline.Message { + message := cnclient.AcquireMessage() + message.SetMessageType(pipeline.Method_PipelineStreamFinish) + message.SetSid(pipeline.Status_Last) + message.SetID(streamID) + message.RequestedTeardownMode = pipeline.StreamTeardownMode_FinishAck + return message +} + +func (sender *messageSenderOnClient) finishStreamForReuse() bool { + var senderDone <-chan struct{} + sender.stateMu.Lock() + allowCleanupCancellation := sender.allowCleanupCancellation + sender.stateMu.Unlock() + cancelCtx := sender.ctx + if allowCleanupCancellation { + cancelCtx = nil + } + if cancelCtx != nil && cancelCtx.Err() != nil { + return false + } + if cancelCtx != nil { + senderDone = cancelCtx.Done() + } + finishCtx, cancel := context.WithTimeout(context.Background(), pipelineStreamFinishClientTimeout) + defer cancel() + streamID := sender.streamSender.ID() + if err := sender.streamSender.Send(finishCtx, generatePipelineStreamFinishMessage(streamID)); err != nil { + return false + } + select { + case value, ok := <-sender.receiveCh: + if !ok || value == nil { + sender.markStreamClosed() + return false + } + message, ok := value.(*pipeline.Message) + return ok && message.GetID() == streamID && + message.GetCmd() == pipeline.Method_PipelineStreamFinishAck && + message.GetSid() == pipeline.Status_MessageEnd && len(message.GetErr()) == 0 && + message.GetAcceptedTeardownMode() == pipeline.StreamTeardownMode_FinishAck + case <-finishCtx.Done(): + return false + case <-senderDone: + return false + } +} + func generateStopSendingMessage(streamID uint64) *pipeline.Message { message := cnclient.AcquireMessage() message.SetMessageType(pipeline.Method_StopSending) @@ -661,17 +790,38 @@ func (sender *messageSenderOnClient) dealRemoteAnalysis(p models.PhyPlan) { } func (sender *messageSenderOnClient) close() { - // Ensure Gauge is decremented exactly once when this sender is torn down, including when - // alreadyClose is true (stream already closed by remote); otherwise we'd leak the gauge. - defer sender.gaugeDecOnce.Do(func() { v2.PipelineMessageSenderGauge.Dec() }) - - sender.waitingTheStopResponse() - - if sender.ctxCancel != nil { - sender.ctxCancel() - } - if sender.alreadyClose { - return - } - _ = sender.streamSender.Close(true) + sender.closeOnce.Do(func() { + // Ensure Gauge is decremented exactly once when this sender is torn down, including when + // alreadyClose is true (stream already closed by remote); otherwise we'd leak the gauge. + defer sender.gaugeDecOnce.Do(func() { v2.PipelineMessageSenderGauge.Dec() }) + + sender.waitingTheStopResponse() + sender.stateMu.Lock() + alreadyClose, reuseEligible := sender.alreadyClose, sender.reuseEligible + sender.stateMu.Unlock() + if !alreadyClose && reuseEligible && sender.finishStreamForReuse() { + v2.PipelineStreamTeardownCounter.WithLabelValues("client_reuse").Inc() + if sender.ctxCancel != nil { + sender.ctxCancel() + } + if err := sender.streamSender.Close(false); err != nil { + _ = sender.streamSender.Close(true) + } + return + } + if reuseEligible { + v2.PipelineStreamTeardownCounter.WithLabelValues("client_fin_failed_close").Inc() + } else { + v2.PipelineStreamTeardownCounter.WithLabelValues("client_legacy_or_poisoned_close").Inc() + } + sender.stateMu.Lock() + alreadyClose = sender.alreadyClose + sender.stateMu.Unlock() + if sender.ctxCancel != nil { + sender.ctxCancel() + } + if !alreadyClose { + _ = sender.streamSender.Close(true) + } + }) } diff --git a/pkg/sql/compile/remoterunClient_test.go b/pkg/sql/compile/remoterunClient_test.go index 450c6038e1e80..1dd2779e75b31 100644 --- a/pkg/sql/compile/remoterunClient_test.go +++ b/pkg/sql/compile/remoterunClient_test.go @@ -33,6 +33,7 @@ import ( "github.com/matrixorigin/matrixone/pkg/common/mpool" "github.com/matrixorigin/matrixone/pkg/common/runtime" "github.com/matrixorigin/matrixone/pkg/defines" + "github.com/matrixorigin/matrixone/pkg/pb/pipeline" "github.com/matrixorigin/matrixone/pkg/pb/plan" "github.com/matrixorigin/matrixone/pkg/sql/colexec" "github.com/matrixorigin/matrixone/pkg/sql/colexec/connector" @@ -159,6 +160,16 @@ func TestNewMessageSenderOnClientReturnsErrorWithoutServiceRuntime(t *testing.T) require.Contains(t, err.Error(), "service runtime is not initialized") } +func TestPipelineStreamReuseRuntimeGate(t *testing.T) { + sid := t.Name() + runtime.SetupServiceBasedRuntime(sid, runtime.DefaultRuntime()) + require.True(t, pipelineStreamReuseEnabled(sid)) + runtime.ServiceRuntime(sid).SetGlobalVariables(runtime.EnablePipelineStreamReuse, false) + require.False(t, pipelineStreamReuseEnabled(sid)) + runtime.ServiceRuntime(sid).SetGlobalVariables(runtime.EnablePipelineStreamReuse, true) + require.True(t, pipelineStreamReuseEnabled(sid)) +} + func TestNewMessageSenderOnClientReturnsErrorOnNilStream(t *testing.T) { sid := t.Name() runtime.SetupServiceBasedRuntime(sid, runtime.DefaultRuntime()) @@ -182,6 +193,200 @@ func TestNewMessageSenderOnClientReturnsErrorOnNilStream(t *testing.T) { require.Contains(t, err.Error(), "pipeline stream is not initialized") } +func TestMessageSenderOnClientNegotiatedStreamTeardown(t *testing.T) { + t.Run("negotiated retry terminal needs explicit cleanup authorization", func(t *testing.T) { + sender := &messageSenderOnClient{expectedEnd: pipeline.Method_PrepareDoneNotifyMessage} + message := &pipeline.Message{ + Cmd: pipeline.Method_PrepareDoneNotifyMessage, + Sid: pipeline.Status_MessageEnd, + AcceptedTeardownMode: pipeline.StreamTeardownMode_FinishAck, + } + sender.markTerminal(message, false) + require.True(t, sender.terminalNegotiated) + require.False(t, sender.reuseEligible) + sender.prepareForLocalCleanup() + require.True(t, sender.reuseEligible) + }) + + t.Run("accepted FIN ACK reuses backend", func(t *testing.T) { + ctrl := gomock.NewController(t) + stream := mock_morpc.NewMockStream(ctrl) + responses := make(chan morpc.Message, 1) + responses <- &pipeline.Message{ + Id: 7, + Cmd: pipeline.Method_PipelineStreamFinishAck, + Sid: pipeline.Status_MessageEnd, + AcceptedTeardownMode: pipeline.StreamTeardownMode_FinishAck, + } + stream.EXPECT().ID().Return(uint64(7)) + stream.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, request morpc.Message) error { + message := request.(*pipeline.Message) + require.Equal(t, pipeline.Method_PipelineStreamFinish, message.GetCmd()) + require.Equal(t, pipeline.Status_Last, message.GetSid()) + return nil + }) + stream.EXPECT().Close(false).Return(nil) + sender := &messageSenderOnClient{ + ctx: context.Background(), + streamSender: stream, + receiveCh: responses, + safeToClose: true, + reuseEligible: true, + } + sender.close() + sender.close() + }) + + t.Run("legacy End closes backend", func(t *testing.T) { + ctrl := gomock.NewController(t) + stream := mock_morpc.NewMockStream(ctrl) + stream.EXPECT().Close(true).Return(nil) + sender := &messageSenderOnClient{ + ctx: context.Background(), + streamSender: stream, + safeToClose: true, + } + sender.close() + }) + + t.Run("malformed FIN ACK poisons backend", func(t *testing.T) { + ctrl := gomock.NewController(t) + stream := mock_morpc.NewMockStream(ctrl) + responses := make(chan morpc.Message, 1) + responses <- &pipeline.Message{Id: 9, Cmd: pipeline.Method_PipelineStreamFinishAck, Sid: pipeline.Status_MessageEnd} + stream.EXPECT().ID().Return(uint64(9)) + stream.EXPECT().Send(gomock.Any(), gomock.Any()).Return(nil) + stream.EXPECT().Close(true).Return(nil) + sender := &messageSenderOnClient{ + ctx: context.Background(), + streamSender: stream, + receiveCh: responses, + safeToClose: true, + reuseEligible: true, + } + sender.close() + }) + + t.Run("query cancellation after End poisons backend", func(t *testing.T) { + ctrl := gomock.NewController(t) + stream := mock_morpc.NewMockStream(ctrl) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + stream.EXPECT().Close(true).Return(nil) + sender := &messageSenderOnClient{ + ctx: ctx, + streamSender: stream, + receiveCh: make(chan morpc.Message), + safeToClose: true, + reuseEligible: true, + } + sender.close() + }) + + t.Run("successful local cleanup cancellation still reuses backend", func(t *testing.T) { + ctrl := gomock.NewController(t) + stream := mock_morpc.NewMockStream(ctrl) + ctx, cancel := context.WithCancel(context.Background()) + responses := make(chan morpc.Message, 1) + responses <- &pipeline.Message{ + Id: 11, + Cmd: pipeline.Method_PipelineStreamFinishAck, + Sid: pipeline.Status_MessageEnd, + AcceptedTeardownMode: pipeline.StreamTeardownMode_FinishAck, + } + stream.EXPECT().ID().Return(uint64(11)) + stream.EXPECT().Send(gomock.Any(), gomock.Any()).Return(nil) + stream.EXPECT().Close(false).Return(nil) + sender := &messageSenderOnClient{ + ctx: ctx, + streamSender: stream, + receiveCh: responses, + safeToClose: true, + reuseEligible: true, + } + sender.prepareForLocalCleanup() + cancel() + sender.close() + }) + + t.Run("cancellation before cleanup completion poisons backend", func(t *testing.T) { + ctrl := gomock.NewController(t) + stream := mock_morpc.NewMockStream(ctrl) + pipelineCtx, cancelPipeline := context.WithCancel(context.Background()) + stream.EXPECT().Close(true).Return(nil) + sender := &messageSenderOnClient{ + ctx: pipelineCtx, + streamSender: stream, + receiveCh: make(chan morpc.Message), + safeToClose: true, + reuseEligible: true, + } + cancelPipeline() + sender.close() + }) + + t.Run("FIN ACK timeout poisons backend", func(t *testing.T) { + ctrl := gomock.NewController(t) + stream := mock_morpc.NewMockStream(ctrl) + stream.EXPECT().ID().Return(uint64(10)) + stream.EXPECT().Send(gomock.Any(), gomock.Any()).Return(nil) + stream.EXPECT().Close(true).Return(nil) + oldTimeout := pipelineStreamFinishClientTimeout + pipelineStreamFinishClientTimeout = 10 * time.Millisecond + t.Cleanup(func() { pipelineStreamFinishClientTimeout = oldTimeout }) + sender := &messageSenderOnClient{ + ctx: context.Background(), + streamSender: stream, + receiveCh: make(chan morpc.Message), + safeToClose: true, + reuseEligible: true, + } + sender.close() + }) + + t.Run("clean StopSending End reuses backend", func(t *testing.T) { + ctrl := gomock.NewController(t) + stream := mock_morpc.NewMockStream(ctrl) + responses := make(chan morpc.Message, 2) + responses <- &pipeline.Message{ + Id: 12, + Cmd: pipeline.Method_PipelineMessage, + Sid: pipeline.Status_MessageEnd, + AcceptedTeardownMode: pipeline.StreamTeardownMode_FinishAck, + } + responses <- &pipeline.Message{ + Id: 12, + Cmd: pipeline.Method_PipelineStreamFinishAck, + Sid: pipeline.Status_MessageEnd, + AcceptedTeardownMode: pipeline.StreamTeardownMode_FinishAck, + } + stream.EXPECT().ID().Return(uint64(12)).Times(2) + gomock.InOrder( + stream.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, request morpc.Message) error { + require.Equal(t, pipeline.Method_StopSending, request.(*pipeline.Message).GetCmd()) + return nil + }), + stream.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, request morpc.Message) error { + require.Equal(t, pipeline.Method_PipelineStreamFinish, request.(*pipeline.Message).GetCmd()) + return nil + }), + ) + stream.EXPECT().Close(false).Return(nil) + sender := &messageSenderOnClient{ + ctx: context.Background(), + streamSender: stream, + receiveCh: responses, + safeToClose: false, + expectedEnd: pipeline.Method_PipelineMessage, + allowCleanupCancellation: true, + } + sender.close() + }) +} + func TestRemoteRun(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/pkg/sql/compile/remoterunServer.go b/pkg/sql/compile/remoterunServer.go index d0b23646ff80b..1f0bb05dd0ebe 100644 --- a/pkg/sql/compile/remoterunServer.go +++ b/pkg/sql/compile/remoterunServer.go @@ -78,10 +78,15 @@ func CnServerMessageHandler( messageAcquirer func() morpc.Message) (err error) { startTime := time.Now() + var lifecycle *pipelineStreamLifecycle defer func() { v2.PipelineServerDurationHistogram.Observe(time.Since(startTime).Seconds()) if e := recover(); e != nil { + if lifecycle != nil { + lifecycle.remove() + lifecycle.markCleaned() + } err = moerr.ConvertPanicError(ctx, e) getLogger(lockService.GetConfig().ServiceID).Error("panic in CnServerMessageHandler", zap.String("error", err.Error())) @@ -102,20 +107,39 @@ func CnServerMessageHandler( if colexecServer == nil { return moerr.NewInternalErrorf(ctx, "colexec server is not initialized for CN %s", lockService.GetConfig().ServiceID) } + if msg.GetCmd() == pipeline.Method_PipelineStreamFinish { + return handlePipelineStreamFinish(ctx, msg, cs, messageAcquirer) + } // prepare the receiver structure, just for easy using the `send` method. receiver := newMessageReceiverOnServer(ctx, serverAddress, msg, cs, messageAcquirer, storageEngine, fileService, lockService, queryClient, HaKeeper, udfService, txnClient, autoIncreaseCM, colexecServer) - // how to handle the *pipeline.Message. - err = handlePipelineMessage(&receiver) + finishNegotiated := false + if receiver.supportsFinishAck() { + lifecycle, err = registerPipelineStreamLifecycle(receiver.clientSession, receiver.messageId) + finishNegotiated = err == nil + if err != nil { + return err + } + receiver.acceptedTeardownMode = pipeline.StreamTeardownMode_FinishAck + } + + // Register negotiated lifecycle ownership before execution. A locally + // requested StopSending may make execution return an error, but its terminal + // response and FIN still need the same cleanup barrier. + handlerErr := handlePipelineMessage(&receiver) + responseSent := false if receiver.messageTyp != pipeline.Method_StopSending { // stop message only close a running pipeline, there is no need to reply the finished-message. - if err != nil { - err = receiver.sendError(err) + if handlerErr != nil { + err = receiver.sendError(handlerErr) } else { err = receiver.sendEndMessage() } + responseSent = err == nil + } else { + err = handlerErr } // if this message is responsible for the execution of certain pipelines, they should be ended after message processing is completed. @@ -123,7 +147,19 @@ func CnServerMessageHandler( // keep listening until connection was closed // to prevent some strange handle order between 'stop sending message' and others. // todo: it is tcp connection now. should be very careful, we should listen to stream context next day. - if err == nil { + if responseSent && finishNegotiated { + finishReceived := lifecycle.waitForFinish(receiver.messageCtx, receiver.connectionCtx) + receiver.colexecServer.RemoveRelatedPipeline(receiver.clientSession, receiver.messageId) + lifecycle.markCleaned() + if !finishReceived { + _ = receiver.clientSession.Close() + } + return nil + } + if lifecycle != nil { + lifecycle.remove() + } + if handlerErr == nil && err == nil { receiver.waitUntilDisconnectedOrCancelled() } receiver.colexecServer.RemoveRelatedPipeline(receiver.clientSession, receiver.messageId) @@ -461,6 +497,9 @@ type messageReceiverOnServer struct { needNotReply bool + requestedTeardownMode pipeline.StreamTeardownMode + acceptedTeardownMode pipeline.StreamTeardownMode + waitRegistrationTimeout time.Duration colexecServer *colexec.Server @@ -485,15 +524,16 @@ func newMessageReceiverOnServer( colexecServer *colexec.Server) messageReceiverOnServer { receiver := messageReceiverOnServer{ - messageCtx: ctx, - connectionCtx: cs.SessionCtx(), - messageId: m.GetId(), - messageTyp: m.GetCmd(), - clientSession: cs, - messageAcquirer: messageAcquirer, - maxMessageSize: maxMessageSizeToMoRpc, - needNotReply: m.NeedNotReply, - colexecServer: colexecServer, + messageCtx: ctx, + connectionCtx: cs.SessionCtx(), + messageId: m.GetId(), + messageTyp: m.GetCmd(), + clientSession: cs, + messageAcquirer: messageAcquirer, + maxMessageSize: maxMessageSizeToMoRpc, + needNotReply: m.NeedNotReply, + requestedTeardownMode: m.GetRequestedTeardownMode(), + colexecServer: colexecServer, } receiver.cnInformation = cnInformation{ cnAddr: cnAddr, @@ -528,6 +568,14 @@ func newMessageReceiverOnServer( return receiver } +func (receiver *messageReceiverOnServer) supportsFinishAck() bool { + if receiver.requestedTeardownMode != pipeline.StreamTeardownMode_FinishAck { + return false + } + return receiver.messageTyp == pipeline.Method_PipelineMessage || + receiver.messageTyp == pipeline.Method_PrepareDoneNotifyMessage +} + func (receiver *messageReceiverOnServer) acquireMessage() (*pipeline.Message, error) { message, ok := receiver.messageAcquirer().(*pipeline.Message) if !ok { @@ -597,6 +645,8 @@ func (receiver *messageReceiverOnServer) sendError( } message.SetID(receiver.messageId) message.SetSid(pipeline.Status_MessageEnd) + message.SetMessageType(receiver.messageTyp) + message.AcceptedTeardownMode = receiver.acceptedTeardownMode if errInfo != nil { message.SetMoError(receiver.messageCtx, errInfo) } @@ -657,6 +707,7 @@ func (receiver *messageReceiverOnServer) sendEndMessage() error { message.SetSid(pipeline.Status_MessageEnd) message.SetID(receiver.messageId) message.SetMessageType(receiver.messageTyp) + message.AcceptedTeardownMode = receiver.acceptedTeardownMode jsonData, err := json.MarshalIndent(receiver.phyPlan, "", " ") if err != nil { diff --git a/pkg/sql/compile/remoterun_lifecycle.go b/pkg/sql/compile/remoterun_lifecycle.go new file mode 100644 index 0000000000000..3694817e91e2c --- /dev/null +++ b/pkg/sql/compile/remoterun_lifecycle.go @@ -0,0 +1,163 @@ +// Copyright 2026 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package compile + +import ( + "context" + "sync" + "time" + + "github.com/matrixorigin/matrixone/pkg/common/moerr" + "github.com/matrixorigin/matrixone/pkg/common/morpc" + "github.com/matrixorigin/matrixone/pkg/pb/pipeline" + v2 "github.com/matrixorigin/matrixone/pkg/util/metric/v2" +) + +var pipelineStreamFinishTimeout = 30 * time.Second + +type pipelineStreamLifecycleKey struct { + session morpc.ClientSession + id uint64 +} + +type pipelineStreamFinishRequest struct { + token morpc.StreamTerminalToken +} + +type pipelineStreamLifecycle struct { + key pipelineStreamLifecycleKey + finishC chan pipelineStreamFinishRequest + cleanedC chan struct{} + cleanOnce sync.Once +} + +var pipelineStreamLifecycles sync.Map + +func registerPipelineStreamLifecycle( + cs morpc.ClientSession, + id uint64, +) (*pipelineStreamLifecycle, error) { + lifecycle := &pipelineStreamLifecycle{ + key: pipelineStreamLifecycleKey{session: cs, id: id}, + finishC: make(chan pipelineStreamFinishRequest, 1), + cleanedC: make(chan struct{}), + } + if _, loaded := pipelineStreamLifecycles.LoadOrStore(lifecycle.key, lifecycle); loaded { + _ = cs.Close() + return nil, moerr.NewInvalidStateNoCtx("duplicate pipeline stream lifecycle") + } + v2.PipelineStreamLifecycleGauge.Inc() + return lifecycle, nil +} + +func (lifecycle *pipelineStreamLifecycle) remove() { + if pipelineStreamLifecycles.CompareAndDelete(lifecycle.key, lifecycle) { + v2.PipelineStreamLifecycleGauge.Dec() + } +} + +func (lifecycle *pipelineStreamLifecycle) markCleaned() { + lifecycle.cleanOnce.Do(func() { close(lifecycle.cleanedC) }) +} + +// waitForFinish starts only after the accepted End response has been flushed. +// A false return means the connection is no longer safe to reuse. +func (lifecycle *pipelineStreamLifecycle) waitForFinish( + messageCtx context.Context, + connectionCtx context.Context, +) bool { + timer := time.NewTimer(pipelineStreamFinishTimeout) + defer timer.Stop() + select { + case <-lifecycle.finishC: + return true + case <-messageCtx.Done(): + v2.PipelineStreamTeardownCounter.WithLabelValues("server_message_cancel").Inc() + case <-connectionCtx.Done(): + v2.PipelineStreamTeardownCounter.WithLabelValues("server_connection_close").Inc() + case <-timer.C: + v2.PipelineStreamTeardownCounter.WithLabelValues("server_fin_timeout").Inc() + } + lifecycle.remove() + return false +} + +func handlePipelineStreamFinish( + ctx context.Context, + message *pipeline.Message, + cs morpc.ClientSession, + messageAcquirer func() morpc.Message, +) error { + poison := func(err error) error { + _ = cs.Close() + return err + } + if message.GetSid() != pipeline.Status_Last || + message.GetRequestedTeardownMode() != pipeline.StreamTeardownMode_FinishAck { + return poison(moerr.NewInvalidInputNoCtx("pipeline stream FIN must be a Last message")) + } + token, ok := morpc.StreamTerminalTokenFromContext(ctx) + if !ok { + return poison(moerr.NewInvalidStateNoCtx("pipeline stream FIN has no validated stream token")) + } + key := pipelineStreamLifecycleKey{session: cs, id: message.GetID()} + value, ok := pipelineStreamLifecycles.Load(key) + if !ok { + return poison(moerr.NewInvalidStateNoCtx("pipeline stream FIN has no active lifecycle")) + } + lifecycle := value.(*pipelineStreamLifecycle) + coordinationCtx, coordinationCancel := context.WithTimeout(ctx, pipelineStreamFinishTimeout) + defer coordinationCancel() + select { + case lifecycle.finishC <- pipelineStreamFinishRequest{token: token}: + case <-cs.SessionCtx().Done(): + return poison(moerr.NewStreamClosedNoCtx()) + case <-coordinationCtx.Done(): + lifecycle.remove() + return poison(coordinationCtx.Err()) + } + + select { + case <-lifecycle.cleanedC: + case <-cs.SessionCtx().Done(): + lifecycle.remove() + return moerr.NewStreamClosedNoCtx() + case <-coordinationCtx.Done(): + lifecycle.remove() + return poison(coordinationCtx.Err()) + } + + finisher, ok := cs.(morpc.StreamFinisher) + if !ok { + lifecycle.remove() + return poison(moerr.NewInvalidStateNoCtx("client session cannot finish a stream")) + } + response, ok := messageAcquirer().(*pipeline.Message) + if !ok { + lifecycle.remove() + return poison(moerr.NewInvalidStateNoCtx("pipeline FIN acquired an invalid response")) + } + response.SetID(message.GetID()) + response.SetSid(pipeline.Status_MessageEnd) + response.SetMessageType(pipeline.Method_PipelineStreamFinishAck) + response.AcceptedTeardownMode = pipeline.StreamTeardownMode_FinishAck + + finishCtx, cancel := context.WithTimeout(context.Background(), pipelineStreamFinishTimeout) + defer cancel() + start := time.Now() + err := finisher.FinishStream(finishCtx, token, response) + v2.PipelineStreamFinishDurationHistogram.Observe(time.Since(start).Seconds()) + if err == nil { + v2.PipelineStreamTeardownCounter.WithLabelValues("server_fin_ack").Inc() + } else { + v2.PipelineStreamTeardownCounter.WithLabelValues("server_fin_ack_error").Inc() + } + lifecycle.remove() + return err +} diff --git a/pkg/sql/compile/remoterun_lifecycle_test.go b/pkg/sql/compile/remoterun_lifecycle_test.go new file mode 100644 index 0000000000000..45a1821f394d0 --- /dev/null +++ b/pkg/sql/compile/remoterun_lifecycle_test.go @@ -0,0 +1,59 @@ +// Copyright 2026 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package compile + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/matrixorigin/matrixone/pkg/common/morpc" + "github.com/matrixorigin/matrixone/pkg/common/morpc/mock_morpc" + "github.com/matrixorigin/matrixone/pkg/pb/pipeline" + "github.com/stretchr/testify/require" +) + +func TestPipelineStreamLifecycleTimeoutRemovesRegistration(t *testing.T) { + ctrl := gomock.NewController(t) + session := mock_morpc.NewMockClientSession(ctrl) + lifecycle, err := registerPipelineStreamLifecycle(session, 101) + require.NoError(t, err) + oldTimeout := pipelineStreamFinishTimeout + pipelineStreamFinishTimeout = 10 * time.Millisecond + t.Cleanup(func() { + pipelineStreamFinishTimeout = oldTimeout + lifecycle.remove() + }) + + require.False(t, lifecycle.waitForFinish(context.Background(), context.Background())) + _, exists := pipelineStreamLifecycles.Load(lifecycle.key) + require.False(t, exists) +} + +func TestPipelineStreamLifecycleDuplicatePoisonsSession(t *testing.T) { + ctrl := gomock.NewController(t) + session := mock_morpc.NewMockClientSession(ctrl) + session.EXPECT().Close().Return(nil) + first, err := registerPipelineStreamLifecycle(session, 102) + require.NoError(t, err) + defer first.remove() + _, err = registerPipelineStreamLifecycle(session, 102) + require.Error(t, err) +} + +func TestPipelineStreamFinishWithoutValidatedTokenPoisonsSession(t *testing.T) { + ctrl := gomock.NewController(t) + session := mock_morpc.NewMockClientSession(ctrl) + session.EXPECT().Close().Return(nil) + err := handlePipelineStreamFinish( + context.Background(), + &pipeline.Message{Id: 103, Cmd: pipeline.Method_PipelineStreamFinish, Sid: pipeline.Status_Last}, + session, + func() morpc.Message { return &pipeline.Message{} }, + ) + require.Error(t, err) +} diff --git a/pkg/sql/compile/remoterun_test.go b/pkg/sql/compile/remoterun_test.go index 952eb181c26c6..977b87aa68dfd 100644 --- a/pkg/sql/compile/remoterun_test.go +++ b/pkg/sql/compile/remoterun_test.go @@ -1782,6 +1782,7 @@ var _ morpc.Stream = &fakeStreamSender{} type fakeStreamSender struct { // how many packages were sent. sentCnt int + sent []morpc.Message // return it during next send. nextSendError error @@ -1791,6 +1792,7 @@ func (s *fakeStreamSender) ID() uint64 { return 0 } func (s *fakeStreamSender) Send(ctx context.Context, request morpc.Message) error { if s.nextSendError == nil { s.sentCnt++ + s.sent = append(s.sent, request) } return s.nextSendError } @@ -1944,30 +1946,39 @@ func TestBuildRemoteDispatchReceiverRootReusesEarlyRegistration(t *testing.T) { func Test_MessageSenderSendPipeline(t *testing.T) { sender := messageSenderOnClient{ - ctx: context.Background(), - streamSender: &fakeStreamSender{}, + ctx: context.Background(), + streamSender: &fakeStreamSender{}, + requestFinishAck: true, } { // there should only send one time if this is just a small data. sender.streamSender.(*fakeStreamSender).sentCnt = 0 + sender.streamSender.(*fakeStreamSender).sent = nil sender.streamSender.(*fakeStreamSender).nextSendError = nil err := sender.sendPipeline(make([]byte, 10), make([]byte, 10), true, 100, "") require.Nil(t, err) require.Equal(t, 1, sender.streamSender.(*fakeStreamSender).sentCnt) + require.Equal(t, pipeline.StreamTeardownMode_FinishAck, + sender.streamSender.(*fakeStreamSender).sent[0].(*pipeline.Message).GetRequestedTeardownMode()) } { // there should be cut as multiple message to send for a big data. sender.streamSender.(*fakeStreamSender).sentCnt = 0 + sender.streamSender.(*fakeStreamSender).sent = nil sender.streamSender.(*fakeStreamSender).nextSendError = nil err := sender.sendPipeline(make([]byte, 10), make([]byte, 10), true, 5, "") require.Nil(t, err) require.True(t, sender.streamSender.(*fakeStreamSender).sentCnt > 1) + for _, sent := range sender.streamSender.(*fakeStreamSender).sent { + require.Equal(t, pipeline.StreamTeardownMode_FinishAck, + sent.(*pipeline.Message).GetRequestedTeardownMode()) + } } { diff --git a/pkg/sql/compile/scope.go b/pkg/sql/compile/scope.go index c50746eecfb5c..e23012a9f995e 100644 --- a/pkg/sql/compile/scope.go +++ b/pkg/sql/compile/scope.go @@ -457,6 +457,9 @@ func (s *Scope) RemoteRun(c *Compile) error { // sender should be closed after cleanup (tell the children-pipeline that query was done). if sender != nil { + if err == nil { + sender.prepareForLocalCleanup() + } sender.close() } return runErr @@ -864,6 +867,9 @@ type notifyMessageSenderFactory func( // clean do final work for a notifyMessageResult. func (r *notifyMessageResult) clean(proc *process.Process) { if r.sender != nil { + if r.err == nil { + r.sender.prepareForLocalCleanup() + } r.sender.close() } if r.err != nil { @@ -920,10 +926,12 @@ func (s *Scope) sendNotifyMessageWithFactory( closeWithError(err, s.Proc.Reg.MergeReceivers[receiverIdx], nil) return } - message := cnclient.AcquireMessage() message.SetID(sender.streamSender.ID()) message.SetMessageType(pbpipeline.Method_PrepareDoneNotifyMessage) + if sender.requestFinishAck { + message.RequestedTeardownMode = pbpipeline.StreamTeardownMode_FinishAck + } message.NeedNotReply = false message.Uuid = uuid @@ -931,14 +939,17 @@ func (s *Scope) sendNotifyMessageWithFactory( closeWithError(errSend, s.Proc.Reg.MergeReceivers[receiverIdx], sender) return } - sender.safeToClose = false - sender.alreadyClose = false + sender.markStreamActive(pbpipeline.Method_PrepareDoneNotifyMessage) err = receiveMsgAndForward(sender, s.Proc.Reg.MergeReceivers[receiverIdx]) if !isRemoteDispatchNotRegisteredYetError(err) { closeWithError(err, s.Proc.Reg.MergeReceivers[receiverIdx], sender) return } + // "not registered yet" is an expected retry response. The + // negotiated terminal response proves the old attempt can use + // FIN/ACK after its server cleanup barrier. + sender.prepareForLocalCleanup() sender.close() select { diff --git a/pkg/sql/compile/scope_test.go b/pkg/sql/compile/scope_test.go index 2c732325523e6..3deb79935465d 100644 --- a/pkg/sql/compile/scope_test.go +++ b/pkg/sql/compile/scope_test.go @@ -1537,7 +1537,7 @@ func TestNotifyMessageClean(t *testing.T) { streamSender: ff, safeToClose: true, } - // no matter error happens or not, clean method should close the sender. + // Repeated cleanup attempts must retire a sender exactly once. n1 := notifyMessageResult{ sender: sender, err: moerr.NewInternalErrorNoCtx("there is an error."), @@ -1551,7 +1551,7 @@ func TestNotifyMessageClean(t *testing.T) { require.Equal(t, 1, ff.number) n2.clean(proc) - require.Equal(t, 2, ff.number) + require.Equal(t, 1, ff.number) } func TestSuppressRemoteRunCancelError(t *testing.T) { diff --git a/pkg/util/metric/v2/metrics.go b/pkg/util/metric/v2/metrics.go index 2620363612e85..c7f8326b6c58a 100644 --- a/pkg/util/metric/v2/metrics.go +++ b/pkg/util/metric/v2/metrics.go @@ -226,6 +226,7 @@ func initRPCMetrics() { registry.MustRegister(rpcSendingQueueSizeGauge) registry.MustRegister(rpcSendingBatchSizeGauge) registry.MustRegister(rpcServerSessionSizeGauge) + registry.MustRegister(rpcServerStreamStateGauge) registry.MustRegister(rpcGCRegisteredClientsGauge) registry.MustRegister(rpcGCChannelQueueLengthGauge) registry.MustRegister(rpcBackendActiveRequestsGauge) @@ -274,6 +275,9 @@ func initPipelineMetrics() { registry.MustRegister(PipelineServerDurationHistogram) registry.MustRegister(pipelineStreamGauge) registry.MustRegister(PipelineCleanupEventCounter) + registry.MustRegister(PipelineStreamTeardownCounter) + registry.MustRegister(PipelineStreamLifecycleGauge) + registry.MustRegister(PipelineStreamFinishDurationHistogram) } func initLogServiceMetrics() { diff --git a/pkg/util/metric/v2/morpc.go b/pkg/util/metric/v2/morpc.go index 5003caea5ccc6..128d0b3b3e1dd 100644 --- a/pkg/util/metric/v2/morpc.go +++ b/pkg/util/metric/v2/morpc.go @@ -181,6 +181,14 @@ var ( Help: "Size of server sessions size.", }, []string{"name"}) + rpcServerStreamStateGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "mo", + Subsystem: "rpc", + Name: "server_stream_state_size", + Help: "Current server-side stream sequence and fragment-cache entries.", + }, []string{"name", "type"}) + rpcGCRegisteredClientsGauge = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "mo", @@ -340,6 +348,10 @@ func NewRPCServerSessionSizeGaugeByName(name string) prometheus.Gauge { return rpcServerSessionSizeGauge.WithLabelValues(name) } +func NewRPCServerStreamStateGaugeByName(name, stateType string) prometheus.Gauge { + return rpcServerStreamStateGauge.WithLabelValues(name, stateType) +} + func NewRPCInputCounter() prometheus.Counter { return rpcNetworkBytesCounter.WithLabelValues("input") } diff --git a/pkg/util/metric/v2/pipeline.go b/pkg/util/metric/v2/pipeline.go index dd93abfa6c3f6..9a5f0557b59f4 100644 --- a/pkg/util/metric/v2/pipeline.go +++ b/pkg/util/metric/v2/pipeline.go @@ -44,4 +44,29 @@ var ( Name: "cleanup_event", Help: "Total number of abnormal pipeline cleanup events.", }, []string{"event"}) + + PipelineStreamTeardownCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "mo", + Subsystem: "pipeline", + Name: "stream_teardown_total", + Help: "Pipeline stream teardown outcomes by event.", + }, []string{"event"}) + + PipelineStreamLifecycleGauge = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: "mo", + Subsystem: "pipeline", + Name: "stream_lifecycle_active", + Help: "Current number of server-side FIN lifecycle registrations.", + }) + + PipelineStreamFinishDurationHistogram = prometheus.NewHistogram( + prometheus.HistogramOpts{ + Namespace: "mo", + Subsystem: "pipeline", + Name: "stream_finish_duration_seconds", + Help: "FIN to FIN_ACK completion latency in seconds.", + Buckets: getDurationBuckets(), + }) ) diff --git a/proto/pipeline.proto b/proto/pipeline.proto index a55d77fac6633..6e162cfe16282 100644 --- a/proto/pipeline.proto +++ b/proto/pipeline.proto @@ -33,6 +33,16 @@ enum Method { BatchMessage = 2; PrepareDoneNotifyMessage = 3; // for dispatch StopSending = 4; + PipelineStreamFinish = 5; + PipelineStreamFinishAck = 6; +} + +// StreamTeardownMode negotiates whether a completed logical pipeline stream can +// be retired without closing its exclusive TCP backend. Zero intentionally +// preserves the legacy close-the-backend behavior for rolling upgrades. +enum StreamTeardownMode { + LegacyClose = 0; + FinishAck = 1; } enum Status { @@ -53,6 +63,8 @@ message Message { bytes uuid = 8; bool needNotReply = 9; string debugMsg = 10; + StreamTeardownMode requested_teardown_mode = 11; + StreamTeardownMode accepted_teardown_mode = 12; } message Connector { From f92db6739b65bb6bfc5303b95dfb32bbfaced5e0 Mon Sep 17 00:00:00 2001 From: aptend Date: Wed, 15 Jul 2026 15:20:56 +0800 Subject: [PATCH 2/3] chore: complete remote lifecycle license headers --- pkg/sql/compile/remoterun_lifecycle.go | 6 ++++++ pkg/sql/compile/remoterun_lifecycle_test.go | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/pkg/sql/compile/remoterun_lifecycle.go b/pkg/sql/compile/remoterun_lifecycle.go index 3694817e91e2c..d5205973afec4 100644 --- a/pkg/sql/compile/remoterun_lifecycle.go +++ b/pkg/sql/compile/remoterun_lifecycle.go @@ -5,6 +5,12 @@ // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package compile diff --git a/pkg/sql/compile/remoterun_lifecycle_test.go b/pkg/sql/compile/remoterun_lifecycle_test.go index 45a1821f394d0..67e1f1dbfd643 100644 --- a/pkg/sql/compile/remoterun_lifecycle_test.go +++ b/pkg/sql/compile/remoterun_lifecycle_test.go @@ -2,6 +2,15 @@ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package compile From ea7271bffaaa4ce5ba355082fe563b8d3ff5c4f3 Mon Sep 17 00:00:00 2001 From: aptend Date: Thu, 16 Jul 2026 10:49:42 +0800 Subject: [PATCH 3/3] test(compile): cover pipeline stream lifecycle --- pkg/sql/compile/remoterun_lifecycle.go | 18 ++ pkg/sql/compile/remoterun_lifecycle_test.go | 247 +++++++++++++++++++- 2 files changed, 261 insertions(+), 4 deletions(-) diff --git a/pkg/sql/compile/remoterun_lifecycle.go b/pkg/sql/compile/remoterun_lifecycle.go index d5205973afec4..19160f68a4524 100644 --- a/pkg/sql/compile/remoterun_lifecycle.go +++ b/pkg/sql/compile/remoterun_lifecycle.go @@ -112,6 +112,24 @@ func handlePipelineStreamFinish( if !ok { return poison(moerr.NewInvalidStateNoCtx("pipeline stream FIN has no validated stream token")) } + return handleValidatedPipelineStreamFinish(ctx, message, cs, messageAcquirer, token) +} + +// handleValidatedPipelineStreamFinish coordinates teardown after the morpc IO +// loop has authenticated the terminal stream token. Keeping that validation at +// the public handler boundary makes the coordination state machine testable +// without exposing a way for application code to manufacture terminal tokens. +func handleValidatedPipelineStreamFinish( + ctx context.Context, + message *pipeline.Message, + cs morpc.ClientSession, + messageAcquirer func() morpc.Message, + token morpc.StreamTerminalToken, +) error { + poison := func(err error) error { + _ = cs.Close() + return err + } key := pipelineStreamLifecycleKey{session: cs, id: message.GetID()} value, ok := pipelineStreamLifecycles.Load(key) if !ok { diff --git a/pkg/sql/compile/remoterun_lifecycle_test.go b/pkg/sql/compile/remoterun_lifecycle_test.go index 67e1f1dbfd643..98b7c09dfaad4 100644 --- a/pkg/sql/compile/remoterun_lifecycle_test.go +++ b/pkg/sql/compile/remoterun_lifecycle_test.go @@ -16,6 +16,7 @@ package compile import ( "context" + "errors" "testing" "time" @@ -26,6 +27,43 @@ import ( "github.com/stretchr/testify/require" ) +type lifecycleTestSession struct { + morpc.ClientSession + ctx context.Context + closeCalls int +} + +func (s *lifecycleTestSession) Close() error { + s.closeCalls++ + return nil +} + +func (s *lifecycleTestSession) SessionCtx() context.Context { + return s.ctx +} + +type lifecycleTestFinisherSession struct { + *lifecycleTestSession + finish func(context.Context, morpc.StreamTerminalToken, morpc.Message) error +} + +func (s *lifecycleTestFinisherSession) FinishStream( + ctx context.Context, + token morpc.StreamTerminalToken, + response morpc.Message, +) error { + return s.finish(ctx, token, response) +} + +func newPipelineFinishMessage(id uint64) *pipeline.Message { + return &pipeline.Message{ + Id: id, + Cmd: pipeline.Method_PipelineStreamFinish, + Sid: pipeline.Status_Last, + RequestedTeardownMode: pipeline.StreamTeardownMode_FinishAck, + } +} + func TestPipelineStreamLifecycleTimeoutRemovesRegistration(t *testing.T) { ctrl := gomock.NewController(t) session := mock_morpc.NewMockClientSession(ctrl) @@ -54,15 +92,216 @@ func TestPipelineStreamLifecycleDuplicatePoisonsSession(t *testing.T) { require.Error(t, err) } +func TestPipelineStreamLifecycleWaitTermination(t *testing.T) { + tests := []struct { + name string + prepare func(*pipelineStreamLifecycle) (context.Context, context.Context) + wantFinish bool + }{ + { + name: "finish", + prepare: func(lifecycle *pipelineStreamLifecycle) (context.Context, context.Context) { + lifecycle.finishC <- pipelineStreamFinishRequest{} + return context.Background(), context.Background() + }, + wantFinish: true, + }, + { + name: "message canceled", + prepare: func(*pipelineStreamLifecycle) (context.Context, context.Context) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + return ctx, context.Background() + }, + }, + { + name: "connection closed", + prepare: func(*pipelineStreamLifecycle) (context.Context, context.Context) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + return context.Background(), ctx + }, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &lifecycleTestSession{ctx: context.Background()} + lifecycle, err := registerPipelineStreamLifecycle(session, uint64(200+i)) + require.NoError(t, err) + t.Cleanup(lifecycle.remove) + messageCtx, connectionCtx := tt.prepare(lifecycle) + require.Equal(t, tt.wantFinish, lifecycle.waitForFinish(messageCtx, connectionCtx)) + _, exists := pipelineStreamLifecycles.Load(lifecycle.key) + require.Equal(t, tt.wantFinish, exists) + }) + } +} + +func TestPipelineStreamLifecycleMarkCleanedIsIdempotent(t *testing.T) { + lifecycle := &pipelineStreamLifecycle{cleanedC: make(chan struct{})} + lifecycle.markCleaned() + lifecycle.markCleaned() + select { + case <-lifecycle.cleanedC: + default: + t.Fatal("cleaned lifecycle was not signaled") + } +} + +func TestPipelineStreamFinishRejectsMalformedMessage(t *testing.T) { + tests := []struct { + name string + message *pipeline.Message + }{ + {name: "not last", message: &pipeline.Message{Id: 103, RequestedTeardownMode: pipeline.StreamTeardownMode_FinishAck}}, + {name: "mode not negotiated", message: &pipeline.Message{Id: 104, Sid: pipeline.Status_Last}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &lifecycleTestSession{ctx: context.Background()} + err := handlePipelineStreamFinish( + context.Background(), tt.message, session, + func() morpc.Message { return &pipeline.Message{} }, + ) + require.Error(t, err) + require.Equal(t, 1, session.closeCalls) + }) + } +} + func TestPipelineStreamFinishWithoutValidatedTokenPoisonsSession(t *testing.T) { - ctrl := gomock.NewController(t) - session := mock_morpc.NewMockClientSession(ctrl) - session.EXPECT().Close().Return(nil) + session := &lifecycleTestSession{ctx: context.Background()} err := handlePipelineStreamFinish( context.Background(), - &pipeline.Message{Id: 103, Cmd: pipeline.Method_PipelineStreamFinish, Sid: pipeline.Status_Last}, + newPipelineFinishMessage(105), session, func() morpc.Message { return &pipeline.Message{} }, ) require.Error(t, err) + require.Equal(t, 1, session.closeCalls) +} + +func TestValidatedPipelineStreamFinishRequiresLifecycle(t *testing.T) { + session := &lifecycleTestSession{ctx: context.Background()} + err := handleValidatedPipelineStreamFinish( + context.Background(), newPipelineFinishMessage(106), session, + func() morpc.Message { return &pipeline.Message{} }, + morpc.StreamTerminalToken{}, + ) + require.Error(t, err) + require.Equal(t, 1, session.closeCalls) +} + +func TestValidatedPipelineStreamFinishRequiresFinisher(t *testing.T) { + session := &lifecycleTestSession{ctx: context.Background()} + lifecycle, err := registerPipelineStreamLifecycle(session, 107) + require.NoError(t, err) + t.Cleanup(lifecycle.remove) + lifecycle.markCleaned() + + err = handleValidatedPipelineStreamFinish( + context.Background(), newPipelineFinishMessage(107), session, + func() morpc.Message { return &pipeline.Message{} }, + morpc.StreamTerminalToken{}, + ) + require.Error(t, err) + require.Equal(t, 1, session.closeCalls) + _, exists := pipelineStreamLifecycles.Load(lifecycle.key) + require.False(t, exists) +} + +func TestValidatedPipelineStreamFinishRejectsInvalidResponse(t *testing.T) { + base := &lifecycleTestSession{ctx: context.Background()} + session := &lifecycleTestFinisherSession{ + lifecycleTestSession: base, + finish: func(context.Context, morpc.StreamTerminalToken, morpc.Message) error { + t.Fatal("FinishStream must not run with an invalid response") + return nil + }, + } + lifecycle, err := registerPipelineStreamLifecycle(session, 108) + require.NoError(t, err) + t.Cleanup(lifecycle.remove) + lifecycle.markCleaned() + + err = handleValidatedPipelineStreamFinish( + context.Background(), newPipelineFinishMessage(108), session, + func() morpc.Message { return nil }, + morpc.StreamTerminalToken{}, + ) + require.Error(t, err) + require.Equal(t, 1, session.closeCalls) +} + +func TestValidatedPipelineStreamFinishAck(t *testing.T) { + wantErr := errors.New("finish failed") + tests := []struct { + name string + id uint64 + wantErr error + }{ + {name: "success", id: 109}, + {name: "finish error", id: 110, wantErr: wantErr}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + base := &lifecycleTestSession{ctx: context.Background()} + response := &pipeline.Message{} + session := &lifecycleTestFinisherSession{ + lifecycleTestSession: base, + finish: func(ctx context.Context, _ morpc.StreamTerminalToken, message morpc.Message) error { + require.NoError(t, ctx.Err()) + require.Same(t, response, message) + require.Equal(t, tt.id, response.GetID()) + require.Equal(t, pipeline.Status_MessageEnd, response.GetSid()) + require.Equal(t, pipeline.Method_PipelineStreamFinishAck, response.GetCmd()) + require.Equal(t, pipeline.StreamTeardownMode_FinishAck, response.GetAcceptedTeardownMode()) + return tt.wantErr + }, + } + lifecycle, err := registerPipelineStreamLifecycle(session, tt.id) + require.NoError(t, err) + t.Cleanup(lifecycle.remove) + lifecycle.markCleaned() + + err = handleValidatedPipelineStreamFinish( + context.Background(), newPipelineFinishMessage(tt.id), session, + func() morpc.Message { return response }, + morpc.StreamTerminalToken{}, + ) + require.ErrorIs(t, err, tt.wantErr) + _, exists := pipelineStreamLifecycles.Load(lifecycle.key) + require.False(t, exists) + }) + } +} + +func TestValidatedPipelineStreamFinishTimeoutPoisonsSession(t *testing.T) { + oldTimeout := pipelineStreamFinishTimeout + pipelineStreamFinishTimeout = 10 * time.Millisecond + t.Cleanup(func() { pipelineStreamFinishTimeout = oldTimeout }) + + base := &lifecycleTestSession{ctx: context.Background()} + session := &lifecycleTestFinisherSession{ + lifecycleTestSession: base, + finish: func(context.Context, morpc.StreamTerminalToken, morpc.Message) error { + t.Fatal("FinishStream must not run before cleanup") + return nil + }, + } + lifecycle, err := registerPipelineStreamLifecycle(session, 111) + require.NoError(t, err) + t.Cleanup(lifecycle.remove) + + err = handleValidatedPipelineStreamFinish( + context.Background(), newPipelineFinishMessage(111), session, + func() morpc.Message { return &pipeline.Message{} }, + morpc.StreamTerminalToken{}, + ) + require.ErrorIs(t, err, context.DeadlineExceeded) + require.Equal(t, 1, session.closeCalls) + _, exists := pipelineStreamLifecycles.Load(lifecycle.key) + require.False(t, exists) }