diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 00b68d8c4a..0d5650f3cf 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -20,9 +20,11 @@ set(core_public_header include/tencentcloud/core/AbstractClient.h include/tencentcloud/core/AbstractModel.h include/tencentcloud/core/AsyncCallerContext.h + include/tencentcloud/core/CircuitBreaker.h include/tencentcloud/core/CommonClient.h include/tencentcloud/core/Config.h include/tencentcloud/core/Credential.h + include/tencentcloud/core/DomainFailoverManager.h include/tencentcloud/core/Error.h include/tencentcloud/core/Executor.h include/tencentcloud/core/NetworkProxy.h @@ -43,6 +45,7 @@ set(core_public_header_http set(core_public_header_profile include/tencentcloud/core/profile/ClientProfile.h include/tencentcloud/core/profile/HttpProfile.h + include/tencentcloud/core/profile/RegionBreakerProfile.h ) set(core_public_header_utils @@ -57,8 +60,10 @@ set(core_src src/AbstractClient.cpp src/AbstractModel.cpp src/AsyncCallerContext.cpp + src/CircuitBreaker.cpp src/CommonClient.cpp src/Credential.cpp + src/DomainFailoverManager.cpp src/Executor.cpp src/NetworkProxy.cpp src/Sign.cpp diff --git a/core/include/tencentcloud/core/AbstractClient.h b/core/include/tencentcloud/core/AbstractClient.h index 8a41339433..d16c8a02b6 100644 --- a/core/include/tencentcloud/core/AbstractClient.h +++ b/core/include/tencentcloud/core/AbstractClient.h @@ -21,12 +21,35 @@ #include #include #include +#include +#include #include "AbstractModel.h" #include +#include +#include #include +#include namespace TencentCloud { + /// Abstract base for all generated service clients. + /// + /// Thread safety & lifetime contract: + /// + /// 1. Set*() methods (SetClientProfile, SetCredential, + /// SetNetworkProxy, SetHeader, SetRegion) are NOT thread-safe + /// with concurrent DoRequest / DoRequestAsync calls. Configure + /// the client fully before issuing requests from multiple + /// threads. + /// + /// 2. DoRequestAsync() dispatches work on HttpClient's background + /// worker; its completion callback captures the selected + /// breaker as a std::shared_ptr (NOT `this`). + /// The breaker therefore stays alive for the whole callback + /// regardless of AbstractClient's destruction timing, so the + /// callback never dereferences a dangling client. The + /// destructor still deletes m_httpClient first to join the + /// worker before other members are torn down. class AbstractClient { public: @@ -62,6 +85,28 @@ namespace TencentCloud template void DoRequestAsync(std::string action, Req req, ReqOpts opts, AsyncCompletionHandler handler); + // Result of picking an endpoint for one request. + struct EndpointDecision + { + std::string host; // endpoint to send to (always non-empty) + // Breaker to report the outcome to; nullptr when bypassed + // (failover disabled / non-TencentCloud) or when falling + // through to the bottom fallback (force-send, no report). + std::shared_ptr breaker; + }; + + // Get-or-create the breaker guarding |host| under |origin|. + std::shared_ptr BreakerFor(const std::string &origin, + const std::string &host); + + // Pick the endpoint for this request. See EndpointDecision. + EndpointDecision SelectEndpoint(const std::string &primary_endpoint); + + // Report the outcome to the breaker that Allow()ed this request. + // |breaker| == nullptr is a no-op (bypass / bottom fallback). + static void ReportResult(const std::shared_ptr &breaker, + bool success); + private: Credential m_credential; ClientProfile m_clientProfile; @@ -72,6 +117,37 @@ namespace TencentCloud HttpClient *m_httpClient; std::string m_service; std::map m_headers; + + // Region failover (domain-level circuit breaker). + // + // Breakers are keyed by (origin, host) in a lazily-populated + // registry. The candidate list for a request is built by + // DomainFailoverManager::BuildCandidates(); every candidate + // except the LAST is guarded by a breaker. The last candidate is + // the bottom fallback: it is force-sent without a breaker and + // without reporting (legacy behavior preserved). + // + // shared_ptr is used so async completion callbacks can capture + // the selected breaker directly; the breaker then outlives the + // AbstractClient if needed, independent of destruction order. + struct BreakerRegistry + { + std::mutex mutex; // guards lazy insertion into |map| + // key = origin + "\n" + host + std::map > map; + }; + + // nullptr when region failover is disabled -> fully bypassed. + std::shared_ptr m_breakers; + + void InitRegionBreakers(); + + static bool IsFailoverTriggering(const std::string &error_code); + + HttpClient::HttpResponseOutcome DoRequestWithEndpoint( + const std::string &actionName, const std::string &body, + std::map &headers, + const std::string &endpoint); }; } @@ -81,25 +157,34 @@ void TencentCloud::AbstractClient::DoRequestAsync( { using RequestOutcome = Outcome; const auto& http_profile = m_clientProfile.GetHttpProfile(); - std::string endpoint = http_profile.GetEndpoint(); - if (endpoint.empty()) + std::string primary_endpoint = http_profile.GetEndpoint(); + if (primary_endpoint.empty()) { - endpoint = m_endpoint; + primary_endpoint = m_endpoint; } - std::string::size_type pos = endpoint.find_first_of('.'); + // Pick an endpoint for this request based on circuit breaker state. + EndpointDecision decision = SelectEndpoint(primary_endpoint); + std::string resolved_endpoint = decision.host; + std::shared_ptr breaker = decision.breaker; + + std::string::size_type pos = resolved_endpoint.find_first_of('.'); if (pos != std::string::npos) - m_service = endpoint.substr(0, pos); + m_service = resolved_endpoint.substr(0, pos); else { m_service = "unknown"; - Core::Error err("ClientError", "endpoint `" + endpoint + "` is not valid"); + // Endpoint is syntactically invalid and cannot be used. + // Release the HalfOpen probe slot SelectEndpoint reserved, so + // invalid-endpoint paths don't leak breaker probe capacity. + ReportResult(breaker, /*success=*/false); + Core::Error err("ClientError", "endpoint `" + resolved_endpoint + "` is not valid"); handler(req, RequestOutcome(err)); return; } Url url; - url.SetHost(endpoint); + url.SetHost(resolved_endpoint); HttpProfile::Scheme scheme = http_profile.GetProtocol(); if (scheme == HttpProfile::Scheme::HTTP) url.SetScheme("http"); @@ -145,8 +230,29 @@ void TencentCloud::AbstractClient::DoRequestAsync( m_httpClient->SetReqTimeout(http_profile.GetReqTimeout() * 1000); m_httpClient->SetConnectTimeout(http_profile.GetConnectTimeout() * 1000); - m_httpClient->SendRequestAsync(http_req, [req, handler](HttpClient::HttpResponseOutcome http_resp) + // Align with the synchronous path: honor user-provided CA and + // resolve-IP settings so that async requests respect HTTPS custom + // trust anchors and forced DNS resolution. + m_httpClient->SetCaInfo(http_profile.GetCaInfo()); + m_httpClient->SetCaPath(http_profile.GetCaPath()); + m_httpClient->SetResolveIp(http_profile.GetResolveIp()); + + // Capture the selected breaker (a shared_ptr) so the async callback + // can Report() to it. The breaker is kept alive by the captured + // shared_ptr for the whole callback, independent of AbstractClient's + // destruction order. The callback no longer captures `this`. + m_httpClient->SendRequestAsync(http_req, + [req, handler, breaker]( + HttpClient::HttpResponseOutcome http_resp) mutable { + // Unified report rule: any Allow()==true request MUST Report() + // exactly once. A non-failover error (business 4xx / throttling) + // is treated as a "success" for the endpoint, so the HalfOpen + // probe slot is released (fixes P7). |breaker| nullptr -> no-op. + bool ok = http_resp.IsSuccess() || + !IsFailoverTriggering(http_resp.GetError().GetErrorCode()); + ReportResult(breaker, ok); + if (!http_resp.IsSuccess()) { handler(req, RequestOutcome(http_resp.GetError())); diff --git a/core/include/tencentcloud/core/CircuitBreaker.h b/core/include/tencentcloud/core/CircuitBreaker.h new file mode 100644 index 0000000000..eb82621390 --- /dev/null +++ b/core/include/tencentcloud/core/CircuitBreaker.h @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#ifndef TENCENTCLOUD_CORE_CIRCUITBREAKER_H_ +#define TENCENTCLOUD_CORE_CIRCUITBREAKER_H_ + +#include +#include + +#include + +namespace TencentCloud +{ + +/// Three-state circuit breaker for region failover. +/// +/// Closed -> traffic allowed; failures counted in a sliding window +/// Open -> traffic rejected; after |timeout| enter HalfOpen +/// HalfOpen -> at most |max_requests| concurrent probes; enough +/// successes close it, any failure reopens it +/// +/// Usage: +/// if (breaker.Allow()) { +/// bool ok = SendRequest(); +/// breaker.Report(ok); // MUST pair with every Allow() == true +/// } +class CircuitBreaker +{ +public: + enum class State + { + kClosed, + kOpen, + kHalfOpen, + }; + + CircuitBreaker(); + explicit CircuitBreaker(const RegionBreakerProfile &profile); + + // Holds a std::mutex -- non-copyable, non-movable. + CircuitBreaker(const CircuitBreaker &) = delete; + CircuitBreaker &operator=(const CircuitBreaker &) = delete; + + /// Returns true iff a new request should be allowed to proceed. + /// On true, the caller MUST subsequently call Report() with the + /// outcome; otherwise HalfOpen probe slots leak. + bool Allow(); + + /// Report the outcome of a request that got Allow() == true. + void Report(bool success); + + /// Observational. + State GetState(); + +private: + void LazyAdvanceStateLocked(); + void TransitionToLocked(State new_state); + bool ReadyToOpenLocked() const; + int64_t NowMs() const; + + std::mutex m_mutex; + State m_state; + + // Counters within the current state window; reset on any state + // transition and on Closed-window expiry. + int m_total; + int m_failures; + int m_consecutiveFailures; + + // HalfOpen-only: number of probes Allow()ed but not yet Report()ed. + // Bounded by |m_maxRequests| to avoid flooding a fragile endpoint. + int m_halfOpenInFlight; + + // Absolute timestamp (ms) at which the current window/timeout + // expires. 0 means "no auto timeout" (HalfOpen). + int64_t m_expiryMs; + + // Immutable configuration (derived from RegionBreakerProfile). + int m_maxFailNum; + double m_maxFailPercent; + int64_t m_windowIntervalMs; + int64_t m_timeoutMs; + int m_maxRequests; +}; + +} // namespace TencentCloud + +#endif // TENCENTCLOUD_CORE_CIRCUITBREAKER_H_ diff --git a/core/include/tencentcloud/core/DomainFailoverManager.h b/core/include/tencentcloud/core/DomainFailoverManager.h new file mode 100644 index 0000000000..27bb614333 --- /dev/null +++ b/core/include/tencentcloud/core/DomainFailoverManager.h @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#ifndef TENCENTCLOUD_CORE_DOMAINFAILOVERMANAGER_H_ +#define TENCENTCLOUD_CORE_DOMAINFAILOVERMANAGER_H_ + +#include +#include + +namespace TencentCloud +{ + +/// Pure utility that constructs fallback endpoint candidates from a +/// structured parse of the primary endpoint (see ParseEndpoint). +/// +/// Two mutually exclusive failover modes depending on whether a +/// BackupEndpoint is configured: +/// +/// Mode A (BackupEndpoint configured): +/// Primary -> ResolveBackupEndpoint(backup) (sole fallback / bottom). +/// No TLD fallback is performed. +/// +/// Mode B (BackupEndpoint empty, default): +/// Primary -> next TLD in ring -> second TLD (bottom). +/// TLD ring order: .com -> .com.cn -> .cn -> .com -> ... +/// The fallback target is rebuilt from the parsed primary as +/// service + [kept-modifier] + new-tld +/// so the region segment and dropped-modifiers (intl) are removed, +/// while service and kept-modifiers (ai/internal) are preserved. +/// Region is NO LONGER suppressed: a regional primary also falls +/// back (dropping its region), trading geography drift for +/// availability. +/// Examples: +/// cvm.tencentcloudapi.com -> cvm.tencentcloudapi.com.cn -> cvm.tencentcloudapi.cn +/// hunyuan.ai.tencentcloudapi.com -> hunyuan.ai.tencentcloudapi.com.cn -> hunyuan.ai.tencentcloudapi.cn +/// cvm.intl.tencentcloudapi.com -> cvm.tencentcloudapi.com.cn -> cvm.tencentcloudapi.cn (intl dropped) +/// cvm.ap-shanghai.tencentcloudapi.com -> cvm.tencentcloudapi.com.cn -> cvm.tencentcloudapi.cn (region dropped) +/// +/// This class is stateless; state (closed/open/halfopen) lives in CircuitBreaker. +class DomainFailoverManager +{ +public: + /// Structured parse of a TencentCloud API endpoint. + /// Shape: {service}.[modifier?].[region...].{tld} + /// - modifier is one of the known modifiers (ai/internal/intl), + /// mutually exclusive, at most one. + /// - ORDERING CONTRACT: the modifier MUST immediately follow the + /// service (i.e. always before region). Only the first segment + /// after service is checked. Reversed input (region before + /// modifier, e.g. "cvm.ap-shanghai.ai...") is NOT supported: the + /// modifier there falls into region and is dropped. + /// - region (everything between modifier/service and tld) is NOT + /// stored: it is always dropped on fallback. + struct ParsedEndpoint + { + bool is_tc_domain = false; // false -> not a TC domain + std::string service; // first segment, e.g. "cvm" + std::string modifier; // "ai"/"internal"/"intl"/"" + bool modifier_kept = false; // keep this modifier on fallback? + std::string tld; // normalized lower-case TLD + }; + + /// Parse |endpoint| into its structured parts. If |endpoint| is not a + /// TencentCloud domain, returns a ParsedEndpoint with is_tc_domain=false. + static ParsedEndpoint ParseEndpoint(const std::string &endpoint); + + /// Normalize a user-configured BackupEndpoint (Mode A) using the + /// primary's service name to disambiguate the "bare region" form. + /// - non-TencentCloud domain -> returned as-is + /// - bare region (single segment != primary_service) + /// -> "{primary_service}.{backup}" + /// - complete domain (multi-segment, or single == primary_service) + /// -> returned as-is + /// Modifiers (ai/internal/intl) are never auto-prepended. + static std::string ResolveBackupEndpoint(const std::string &backup_endpoint, + const std::string &primary_service); + + /// Build the ordered candidate endpoint list for one request: + /// [primary, fallback1, fallback2, ...]. The first element is always + /// |primary|; the LAST element is the bottom fallback (the caller + /// must NOT guard it with a breaker -- it is force-sent without + /// reporting, preserving legacy behavior). + /// + /// Built on ParseEndpoint(): Mode A (BackupEndpoint set) appends the + /// normalized backup via ResolveBackupEndpoint(); Mode B (no backup) + /// walks the TLD ring, dropping region and dropped-modifiers (intl) + /// while keeping service and kept-modifiers (ai/internal). + static std::vector BuildCandidates( + const std::string &primary, const std::string &backup_endpoint); + + /// True iff |endpoint| targets tencentcloudapi.com / .com.cn / .cn . + static bool IsTencentCloudDomain(const std::string &endpoint); + + /// Extract the longest matching TLD suffix from |endpoint|, returning + /// one of "tencentcloudapi.com.cn", "tencentcloudapi.com", + /// "tencentcloudapi.cn", or "" if none match. + static std::string ExtractTld(const std::string &endpoint); + +private: + /// TLD ring: .com -> .com.cn -> .cn -> (wraps to .com). + static const std::vector kTldRing; +}; + +} // namespace TencentCloud + +#endif // TENCENTCLOUD_CORE_DOMAINFAILOVERMANAGER_H_ diff --git a/core/include/tencentcloud/core/profile/ClientProfile.h b/core/include/tencentcloud/core/profile/ClientProfile.h index f1178672d8..82f99690f1 100644 --- a/core/include/tencentcloud/core/profile/ClientProfile.h +++ b/core/include/tencentcloud/core/profile/ClientProfile.h @@ -18,6 +18,7 @@ #define TENCENTCLOUD_CORE_CLIENTPROFILE_H_ #include "HttpProfile.h" +#include "RegionBreakerProfile.h" namespace TencentCloud { @@ -43,7 +44,9 @@ namespace TencentCloud ClientProfile(const SignMethod &signMethod, const HttpProfile &httpProfile) : m_httpProfile(httpProfile), m_unsignedPayload(false), - m_signMethod(signMethod) + m_signMethod(signMethod), + m_disableRegionBreaker(false), + m_regionBreakerProfile() { } @@ -56,6 +59,15 @@ namespace TencentCloud void SetHttpProfile(const HttpProfile &httpProfile); HttpProfile GetHttpProfile() const; + /// Region-level failover control. + /// Enabled by default. Call SetDisableRegionBreaker(true) + /// to explicitly disable region failover. + void SetDisableRegionBreaker(bool disabled); + bool GetDisableRegionBreaker() const; + + void SetRegionBreakerProfile(const RegionBreakerProfile &profile); + RegionBreakerProfile GetRegionBreakerProfile() const; + protected: void SetUnsignedPayload(bool flag); bool IsUnsignedPayload(); @@ -64,6 +76,8 @@ namespace TencentCloud HttpProfile m_httpProfile; bool m_unsignedPayload; SignMethod m_signMethod; + bool m_disableRegionBreaker; + RegionBreakerProfile m_regionBreakerProfile; }; } diff --git a/core/include/tencentcloud/core/profile/RegionBreakerProfile.h b/core/include/tencentcloud/core/profile/RegionBreakerProfile.h new file mode 100644 index 0000000000..523b57cfb4 --- /dev/null +++ b/core/include/tencentcloud/core/profile/RegionBreakerProfile.h @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#ifndef TENCENTCLOUD_CORE_REGIONBREAKERPROFILE_H_ +#define TENCENTCLOUD_CORE_REGIONBREAKERPROFILE_H_ + +#include + +namespace TencentCloud +{ + +/// Configuration for region-level circuit breaker (domain failover). +/// +/// Two mutually exclusive failover modes: +/// - If |backup_endpoint| is set: Primary → BackupEndpoint (sole fallback). +/// No TLD-level failover is performed. +/// - If |backup_endpoint| is empty (default): Primary → .com.cn → .cn +/// (TLD ring fallback with region stripped). +class RegionBreakerProfile +{ +public: + RegionBreakerProfile() = default; + + /// @param backup_endpoint The user-specified backup endpoint, in the + /// form ".tencentcloudapi.com" or a full domain. If empty, + /// the BackupEndpoint layer is skipped and fallback proceeds to + /// TLD switching directly. + /// @param max_fail_num Max failure count in the window to trip the breaker. Default 5. + /// @param max_fail_percent Max failure rate in the window to trip the breaker. Default 0.75. + /// @param window_interval Sliding window size in seconds. Default 300. + /// @param timeout Seconds to remain Open before transitioning to HalfOpen. Default 60. + /// @param max_requests Two roles, both active only in HalfOpen state: + /// 1) upper bound on concurrent in-flight + /// probes -- once this many Allow() + /// calls have returned true without a + /// matching Report(), further Allow() + /// calls return false until one completes; + /// 2) total successful probes needed to + /// close the breaker: the transition + /// fires when (total - failures) >= + /// max_requests within the HalfOpen + /// episode. + /// Default 5. + RegionBreakerProfile(const std::string &backup_endpoint, + int max_fail_num, + double max_fail_percent, + int window_interval, + int timeout, + int max_requests) + : m_backupEndpoint(backup_endpoint), + m_maxFailNum(max_fail_num), + m_maxFailPercent(max_fail_percent), + m_windowInterval(window_interval), + m_timeout(timeout), + m_maxRequests(max_requests) + { + } + + void SetBackupEndpoint(const std::string &endpoint) { m_backupEndpoint = endpoint; } + std::string GetBackupEndpoint() const { return m_backupEndpoint; } + + void SetMaxFailNum(int n) { m_maxFailNum = n; } + int GetMaxFailNum() const { return m_maxFailNum; } + + void SetMaxFailPercent(double p) { m_maxFailPercent = p; } + double GetMaxFailPercent() const { return m_maxFailPercent; } + + void SetWindowInterval(int secs) { m_windowInterval = secs; } + int GetWindowInterval() const { return m_windowInterval; } + + void SetTimeout(int secs) { m_timeout = secs; } + int GetTimeout() const { return m_timeout; } + + /// See the constructor comment: max_requests controls BOTH the + /// HalfOpen concurrent-probe cap AND the successful-probe count + /// required to close the breaker. + void SetMaxRequests(int n) { m_maxRequests = n; } + int GetMaxRequests() const { return m_maxRequests; } + +private: + // Empty by default: when not explicitly set, the BackupEndpoint layer + // is skipped and fallback proceeds directly to TLD switching. + std::string m_backupEndpoint; + int m_maxFailNum = 5; + double m_maxFailPercent = 0.75; + int m_windowInterval = 300; + int m_timeout = 60; + int m_maxRequests = 5; +}; + +} // namespace TencentCloud + +#endif // TENCENTCLOUD_CORE_REGIONBREAKERPROFILE_H_ diff --git a/core/src/AbstractClient.cpp b/core/src/AbstractClient.cpp index 2ce94ac965..fc2f1579ce 100644 --- a/core/src/AbstractClient.cpp +++ b/core/src/AbstractClient.cpp @@ -40,12 +40,18 @@ AbstractClient::AbstractClient(const string &endpoint, const string &version, co m_sdkVersion(SDK_VERSION_PREFIX + TENCENTCLOUD_VERSION_STR), m_apiVersion(version), m_httpClient(new HttpClient()), - m_service("") + m_service(""), + m_breakers(nullptr) { + InitRegionBreakers(); } AbstractClient::~AbstractClient() { + // delete m_httpClient first to join its async worker. Breakers are + // held by shared_ptr (in m_breakers and possibly captured by async + // callbacks), so they release automatically and safely regardless + // of destruction timing. delete m_httpClient; } @@ -96,6 +102,7 @@ HttpClient::HttpResponseOutcome AbstractClient::MakeRequest(const AbstractModel& headers.insert(std::make_pair("Content-Type", "application/json")); return DoRequest(actionName, body, headers); } + HttpClient::HttpResponseOutcome AbstractClient::MakeRequestJson(const std::string &actionName, const std::string ¶ms) { std::map headers; @@ -108,15 +115,117 @@ HttpClient::HttpResponseOutcome AbstractClient::MakeRequestOctetStream(const std headers.insert(std::make_pair("Content-Type", "application/octet-stream")); return DoRequest(actionName, body, headers); } + typedef std::map OctetStreamHeadersMap; -HttpClient::HttpResponseOutcome AbstractClient::DoRequest(const std::string &actionName, const std::string &body, std::map &headers) +void AbstractClient::InitRegionBreakers() { - HttpProfile httpProfile = m_clientProfile.GetHttpProfile(); - string endpoint = httpProfile.GetEndpoint(); - if (endpoint == "") - endpoint = m_endpoint; + // Only build the registry when region failover is enabled. + // When disabled, m_breakers stays nullptr and SelectEndpoint + // early-returns without touching it. + if (m_clientProfile.GetDisableRegionBreaker()) + { + return; + } + m_breakers = std::make_shared(); +} + +std::shared_ptr AbstractClient::BreakerFor( + const std::string &origin, const std::string &host) +{ + const std::string key = origin + "\n" + host; + std::lock_guard lock(m_breakers->mutex); + std::map >::iterator it = + m_breakers->map.find(key); + if (it != m_breakers->map.end()) + { + return it->second; // reuse (accumulates state) + } + std::shared_ptr breaker = std::make_shared( + m_clientProfile.GetRegionBreakerProfile()); + m_breakers->map.insert(std::make_pair(key, breaker)); // lazy load + return breaker; +} + +AbstractClient::EndpointDecision AbstractClient::SelectEndpoint( + const std::string &primary_endpoint) +{ + // Bypass: failover disabled / non-TencentCloud endpoint -> send the + // primary directly, no breaker, no report. + if (!m_breakers || + !DomainFailoverManager::IsTencentCloudDomain(primary_endpoint)) + { + EndpointDecision d; + d.host = primary_endpoint; + d.breaker = nullptr; + return d; + } + + RegionBreakerProfile rb_profile = m_clientProfile.GetRegionBreakerProfile(); + std::string backup_ep = rb_profile.GetBackupEndpoint(); + std::vector candidates = + DomainFailoverManager::BuildCandidates(primary_endpoint, backup_ep); + + // The LAST candidate is the bottom fallback: no breaker, force-sent, + // not reported (legacy behavior). Only consult breakers for the + // candidates before it. + const size_t last = candidates.size() - 1; + for (size_t i = 0; i < last; ++i) + { + std::shared_ptr breaker = + BreakerFor(primary_endpoint, candidates[i]); + if (breaker->Allow()) + { + EndpointDecision d; + d.host = candidates[i]; + d.breaker = breaker; // hit -> must Report + return d; + } + } + // All front candidates Open (or single-candidate case) -> fall to + // the bottom; force-send without reporting. + EndpointDecision d; + d.host = candidates[last]; + d.breaker = nullptr; + return d; +} + +void AbstractClient::ReportResult( + const std::shared_ptr &breaker, bool success) +{ + if (breaker) + { + breaker->Report(success); + } +} +bool AbstractClient::IsFailoverTriggering(const std::string &error_code) +{ + // Only network-level errors that are reliably attributable to the + // current endpoint warrant a failover transition: + // DnsError - CURLE_COULDNT_RESOLVE_HOST + // ConnectionError - CURLE_COULDNT_CONNECT + // SSLError - CURLE_PEER_FAILED_VERIFICATION (strong hint + // of DNS hijacking for TencentCloud domains) + // + // Explicitly NOT triggering failover: + // ServiceNetworkError - any HTTP non-2xx; may come from business + // errors (4xx), server faults (5xx), or throttling, and cannot + // be reliably attributed to a per-endpoint network problem. + // Matches HEAD's original behavior. + // NetworkError - e.g. CURLE_OPERATION_TIMEDOUT spans multiple + // stages; local client cert errors cannot be fixed by switching + // domain. + return error_code == "DnsError" || + error_code == "ConnectionError" || + error_code == "SSLError"; +} + +HttpClient::HttpResponseOutcome AbstractClient::DoRequestWithEndpoint( + const std::string &actionName, const std::string &body, + std::map &headers, + const std::string &endpoint) +{ string::size_type pos = endpoint.find_first_of("."); if (pos != string::npos) m_service = endpoint.substr(0, pos); @@ -126,6 +235,8 @@ HttpClient::HttpResponseOutcome AbstractClient::DoRequest(const std::string &act return HttpClient::HttpResponseOutcome(Core::Error("ClientError", "endpoint `"+ endpoint + "` is not valid")); } + HttpProfile httpProfile = m_clientProfile.GetHttpProfile(); + Url url; url.SetHost(endpoint); HttpProfile::Scheme scheme = httpProfile.GetProtocol(); @@ -150,14 +261,14 @@ HttpClient::HttpResponseOutcome AbstractClient::DoRequest(const std::string &act httpRequest.AddHeader("Connection", "Close"); if (headers.size() > 0) { - for(std::map::iterator iter = headers.begin(); iter != headers.end(); iter++) + for (std::map::iterator iter = headers.begin(); iter != headers.end(); ++iter) { httpRequest.AddHeader(iter->first, iter->second); } } if (m_headers.size() > 0) { - for(std::map::iterator iter = m_headers.begin(); iter != m_headers.end(); iter++) + for (std::map::iterator iter = m_headers.begin(); iter != m_headers.end(); ++iter) { httpRequest.AddHeader(iter->first, iter->second); } @@ -174,6 +285,32 @@ HttpClient::HttpResponseOutcome AbstractClient::DoRequest(const std::string &act return m_httpClient->SendRequest(httpRequest); } +HttpClient::HttpResponseOutcome AbstractClient::DoRequest(const std::string &actionName, const std::string &body, std::map &headers) +{ + HttpProfile httpProfile = m_clientProfile.GetHttpProfile(); + string primary_endpoint = httpProfile.GetEndpoint(); + if (primary_endpoint == "") + primary_endpoint = m_endpoint; + + // Pick an endpoint for this request based on circuit breaker state. + // No in-request retry: the breakers make the decision once per + // request. The outcome of that single request is then fed back to + // the breaker. + EndpointDecision decision = SelectEndpoint(primary_endpoint); + + auto outcome = DoRequestWithEndpoint(actionName, body, headers, decision.host); + + // Unified report rule: any Allow()==true request MUST Report() once. + // A non-failover error (business 4xx / throttling) counts as a + // "success" for the endpoint so the HalfOpen probe slot is released + // (fixes P7). decision.breaker == nullptr (bypass / bottom) -> no-op. + bool ok = outcome.IsSuccess() || + !IsFailoverTriggering(outcome.GetError().GetErrorCode()); + ReportResult(decision.breaker, ok); + + return outcome; +} + void AbstractClient::GenerateSignature(HttpRequest &request) { int64_t currentTime; @@ -213,4 +350,4 @@ void AbstractClient::GenerateSignature(HttpRequest &request) string authorization = "TC3-HMAC-SHA256 Credential=" + secretId + "/" + credentialScope + ", SignedHeaders=content-type;host" + ", Signature=" + signature; request.AddHeader("Authorization", authorization); -} \ No newline at end of file +} diff --git a/core/src/CircuitBreaker.cpp b/core/src/CircuitBreaker.cpp new file mode 100644 index 0000000000..e3853236a3 --- /dev/null +++ b/core/src/CircuitBreaker.cpp @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#include + +#include + +using namespace TencentCloud; + +CircuitBreaker::CircuitBreaker() + : CircuitBreaker(RegionBreakerProfile()) +{ +} + +CircuitBreaker::CircuitBreaker(const RegionBreakerProfile &profile) + : m_state(State::kClosed), + m_total(0), + m_failures(0), + m_consecutiveFailures(0), + m_halfOpenInFlight(0), + m_expiryMs(0), + m_maxFailNum(profile.GetMaxFailNum()), + m_maxFailPercent(profile.GetMaxFailPercent()), + m_windowIntervalMs(static_cast(profile.GetWindowInterval()) * 1000), + m_timeoutMs(static_cast(profile.GetTimeout()) * 1000), + m_maxRequests(profile.GetMaxRequests()) +{ + // Start the first Closed-state counting window. + m_expiryMs = NowMs() + m_windowIntervalMs; +} + +int64_t CircuitBreaker::NowMs() const +{ + auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast( + now.time_since_epoch()).count(); +} + +bool CircuitBreaker::Allow() +{ + std::lock_guard lock(m_mutex); + LazyAdvanceStateLocked(); + switch (m_state) + { + case State::kClosed: + return true; + case State::kOpen: + return false; + case State::kHalfOpen: + // Rate-limit concurrent probes during recovery. + if (m_halfOpenInFlight < m_maxRequests) + { + ++m_halfOpenInFlight; + return true; + } + return false; + } + return false; // unreachable +} + +void CircuitBreaker::Report(bool success) +{ + std::lock_guard lock(m_mutex); + LazyAdvanceStateLocked(); + + if (m_state == State::kHalfOpen) + { + // Release the probe slot this Report pairs with. + if (m_halfOpenInFlight > 0) + --m_halfOpenInFlight; + + if (!success) + { + // Any failure during probing reopens the breaker. + TransitionToLocked(State::kOpen); + return; + } + // Count successful probes; close once (total - failures) >= + // max_requests within this HalfOpen episode. + ++m_total; + int successes = m_total - m_failures; + if (successes >= m_maxRequests) + { + TransitionToLocked(State::kClosed); + } + return; + } + + if (m_state == State::kClosed) + { + ++m_total; + if (success) + { + m_consecutiveFailures = 0; + } + else + { + ++m_failures; + ++m_consecutiveFailures; + if (ReadyToOpenLocked()) + { + TransitionToLocked(State::kOpen); + } + } + return; + } + + // State::kOpen: Report with no Allow()==true is a caller bug, but + // be defensive and ignore. +} + +CircuitBreaker::State CircuitBreaker::GetState() +{ + std::lock_guard lock(m_mutex); + LazyAdvanceStateLocked(); + return m_state; +} + +void CircuitBreaker::LazyAdvanceStateLocked() +{ + int64_t now = NowMs(); + switch (m_state) + { + case State::kClosed: + // Sliding window expired: reset counters, start a new one. + if (m_expiryMs != 0 && now >= m_expiryMs) + { + m_total = 0; + m_failures = 0; + m_consecutiveFailures = 0; + m_expiryMs = now + m_windowIntervalMs; + } + break; + case State::kOpen: + // Cooling-off period elapsed: enter HalfOpen to probe. + if (m_expiryMs != 0 && now >= m_expiryMs) + { + TransitionToLocked(State::kHalfOpen); + } + break; + case State::kHalfOpen: + // Driven by request outcomes, no auto timeout. + break; + } +} + +void CircuitBreaker::TransitionToLocked(State new_state) +{ + m_state = new_state; + m_total = 0; + m_failures = 0; + m_consecutiveFailures = 0; + m_halfOpenInFlight = 0; + + int64_t now = NowMs(); + switch (m_state) + { + case State::kClosed: + m_expiryMs = now + m_windowIntervalMs; + break; + case State::kOpen: + m_expiryMs = now + m_timeoutMs; + break; + case State::kHalfOpen: + m_expiryMs = 0; + break; + } +} + +bool CircuitBreaker::ReadyToOpenLocked() const +{ + // Trip condition (two independent branches, either is sufficient): + // (failures >= max_fail_num AND failures/total >= max_fail_percent) + // OR + // (consecutive_failures >= max_fail_num) + if (m_total > 0) + { + double rate = static_cast(m_failures) / + static_cast(m_total); + if (m_failures >= m_maxFailNum && rate >= m_maxFailPercent) + { + return true; + } + } + if (m_consecutiveFailures >= m_maxFailNum) + { + return true; + } + return false; +} diff --git a/core/src/DomainFailoverManager.cpp b/core/src/DomainFailoverManager.cpp new file mode 100644 index 0000000000..7ccabe1ece --- /dev/null +++ b/core/src/DomainFailoverManager.cpp @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#include + +using namespace TencentCloud; + +const std::vector DomainFailoverManager::kTldRing = { + "tencentcloudapi.com", + "tencentcloudapi.com.cn", + "tencentcloudapi.cn", +}; + +namespace +{ + +// True iff |s| ends with |suffix|. +bool EndsWith(const std::string &s, const std::string &suffix) +{ + return s.size() >= suffix.size() && + s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +// Case-insensitive lower-case conversion (ASCII only -- sufficient for +// domain names which are restricted to ASCII per RFC 952/1123). +std::string ToLowerAscii(const std::string &s) +{ + std::string result = s; + for (auto &c : result) + { + if (c >= 'A' && c <= 'Z') + { + c = c - 'A' + 'a'; + } + } + return result; +} + +// Known middle-segment modifiers and whether they are kept on fallback. +// New modifiers only require a row here. +struct ModifierSpec +{ + const char *name; + bool kept; +}; +const ModifierSpec kModifiers[] = { + {"ai", true}, // product identifier, kept + {"internal", true}, // intranet route marker, kept + {"intl", false}, // international-site marker, dropped on fallback +}; + +// Ordered by descending length: longer TLDs must be tested first. +const char *const kTencentCloudTldSuffixes[] = { + ".tencentcloudapi.com.cn", + ".tencentcloudapi.com", + ".tencentcloudapi.cn", +}; + +} // namespace + +DomainFailoverManager::ParsedEndpoint DomainFailoverManager::ParseEndpoint( + const std::string &endpoint) +{ + ParsedEndpoint p; + std::string tld = ExtractTld(endpoint); + if (tld.empty()) + { + return p; // is_tc_domain stays false + } + p.is_tc_domain = true; + p.tld = tld; + + // prefix = everything before ".{tld}" (length-based, case kept). + std::string prefix = endpoint.substr(0, endpoint.size() - tld.size() - 1); + + std::string::size_type dot = prefix.find('.'); + if (dot == std::string::npos) + { + p.service = ToLowerAscii(prefix); // service only, no middle/region + return p; + } + p.service = ToLowerAscii(prefix.substr(0, dot)); + + // First segment after service. + std::string rest = prefix.substr(dot + 1); + std::string::size_type dot2 = rest.find('.'); + std::string second = (dot2 == std::string::npos) ? rest : rest.substr(0, dot2); + + std::string second_lower = ToLowerAscii(second); + for (const ModifierSpec &m : kModifiers) + { + if (second_lower == m.name) + { + p.modifier = m.name; // normalized lower-case form + p.modifier_kept = m.kept; + break; + } + } + // Otherwise no modifier: |second| is the start of region (dropped). + // ORDERING CONTRACT: only |second| (the first segment after service) is + // checked. A modifier appearing after a region segment (reversed input) + // is therefore swallowed by region and dropped -- not supported by design. + return p; +} + +std::string DomainFailoverManager::ResolveBackupEndpoint( + const std::string &backup_endpoint, const std::string &primary_service) +{ + std::string tld = ExtractTld(backup_endpoint); + if (tld.empty()) + { + return backup_endpoint; // non-TencentCloud, as-is + } + std::string prefix = + backup_endpoint.substr(0, backup_endpoint.size() - tld.size() - 1); + + if (prefix.find('.') != std::string::npos) + { + return backup_endpoint; // multi-segment -> complete domain + } + if (prefix == primary_service) + { + return backup_endpoint; // single segment == service -> complete + } + if (primary_service.empty()) + { + return backup_endpoint; // defensive: cannot prepend + } + return primary_service + "." + backup_endpoint; // bare region +} + +std::vector DomainFailoverManager::BuildCandidates( + const std::string &primary, const std::string &backup_endpoint) +{ + std::vector candidates; + candidates.push_back(primary); // first element is always primary + + ParsedEndpoint p = ParseEndpoint(primary); + if (!p.is_tc_domain) + { + return candidates; // non-TencentCloud: no failover + } + + if (!backup_endpoint.empty()) + { + // Mode A: single fallback to the (normalized) backup endpoint. + std::string resolved = ResolveBackupEndpoint(backup_endpoint, p.service); + if (!resolved.empty()) + { + candidates.push_back(resolved); + } + return candidates; + } + + // Mode B: walk the TLD ring starting from the primary's own TLD. + // Region is dropped; kept-modifiers (ai/internal) preserved; intl dropped. + const std::size_t ring_size = kTldRing.size(); + std::size_t origin = ring_size; // sentinel: not found + for (std::size_t i = 0; i < ring_size; ++i) + { + if (kTldRing[i] == p.tld) + { + origin = i; + break; + } + } + if (origin == ring_size) + { + return candidates; // unknown TLD, no fallback + } + + std::string prefix = p.service; + if (p.modifier_kept && !p.modifier.empty()) + { + prefix += "." + p.modifier; + } + for (std::size_t off = 1; off < ring_size; ++off) + { + candidates.push_back(prefix + "." + kTldRing[(origin + off) % ring_size]); + } + return candidates; +} + +bool DomainFailoverManager::IsTencentCloudDomain( + const std::string &endpoint) +{ + // Domain names are case-insensitive (RFC 4343). Normalize before matching. + std::string lower = ToLowerAscii(endpoint); + // Exact suffix match (with a leading '.') so that look-alikes such as + // "tencentcloudapi.company.example.com" are NOT misidentified. + for (const char *suffix : kTencentCloudTldSuffixes) + { + if (EndsWith(lower, suffix)) + { + return true; + } + } + return false; +} + +std::string DomainFailoverManager::ExtractTld(const std::string &endpoint) +{ + // Domain names are case-insensitive. Normalize before matching. + std::string lower = ToLowerAscii(endpoint); + // Match the longest TLD first. Require a leading '.' so that + // "tencentcloudapi.comm" (typo) is not treated as ".com". + for (const char *suffix : kTencentCloudTldSuffixes) + { + if (EndsWith(lower, suffix)) + { + // suffix starts with '.'; skip it when returning. + return std::string(suffix + 1); + } + } + return ""; +} diff --git a/core/src/http/HttpClient.cpp b/core/src/http/HttpClient.cpp index fdc09ab521..40fb40fa89 100644 --- a/core/src/http/HttpClient.cpp +++ b/core/src/http/HttpClient.cpp @@ -263,7 +263,28 @@ HttpClient::HttpResponseOutcome HttpClient::SendRequest(const HttpRequest &reque return HttpResponseOutcome(response); } + case CURLE_COULDNT_RESOLVE_HOST: + return HttpResponseOutcome(Core::Error("DnsError", std::string(errbuf))); + case CURLE_COULDNT_CONNECT: + return HttpResponseOutcome(Core::Error("ConnectionError", std::string(errbuf))); + case CURLE_PEER_FAILED_VERIFICATION: + // Server certificate verification failed (SAN/CN mismatch, expired, + // untrusted CA, etc.). This strongly indicates a server-side issue + // and is a high-confidence signal of DNS hijacking when the target + // is a TencentCloud domain. + return HttpResponseOutcome(Core::Error("SSLError", std::string(errbuf))); default: + // The following errors fall here and are intentionally not treated + // as ConnectionError / SSLError, because they are most likely + // caused by the client itself and cannot be fixed by switching + // region or TLD: + // - CURLE_OPERATION_TIMEDOUT (28): spans DNS/TCP/TLS/req/resp + // stages, cannot be reliably attributed. + // - CURLE_SSL_CONNECT_ERROR (35): TLS handshake failure due to + // protocol/cipher mismatch, usually a client build issue. + // - CURLE_SSL_CERTPROBLEM (58): local client certificate problem. + // - CURLE_SSL_CIPHER (59): requested cipher not supported by + // the local libcurl/OpenSSL build. return HttpResponseOutcome(Core::Error("NetworkError", std::string(errbuf))); } } @@ -389,6 +410,10 @@ void HttpClient::AsyncReqHandler() if (!m_pendingReqs.empty()) { + // Collect failed requests; clean them up after the loop + // to avoid modifying m_pendingReqs during iteration. + std::vector failed_reqs; + for (const auto& async_req : m_pendingReqs) { err = curl_multi_add_handle(m_curlm, async_req->curl_handle); @@ -397,10 +422,21 @@ void HttpClient::AsyncReqHandler() std::cerr << "curl_multi_add_handle:" << curl_multi_strerror(err) << std::endl; async_req->completion_handler( HttpResponseOutcome(Core::Error("ClientError", curl_multi_strerror(err)))); + failed_reqs.push_back(async_req); } } m_pendingReqs.clear(); + // Release resources for requests that failed to add. + // Their curl handles were never added to m_curlm, so + // curl_multi_remove_handle is not needed. + for (auto* ctx : failed_reqs) + { + curl_slist_free_all(ctx->curl_header_buffer); + curl_easy_cleanup(ctx->curl_handle); + delete ctx; + } + int running_handles = 1; err = curl_multi_perform(m_curlm, &running_handles); if (err) @@ -442,7 +478,39 @@ void HttpClient::AsyncReqHandler() { AsyncReqContext* ctx; curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &ctx); + CURLcode result = msg->data.result; + if (result != CURLE_OK) + { + // Classify curl errors the same way as synchronous SendRequest + std::string err_msg(ctx->curl_err_buffer); + switch (result) { + case CURLE_COULDNT_RESOLVE_HOST: + ctx->completion_handler( + HttpResponseOutcome(Core::Error("DnsError", err_msg))); + break; + case CURLE_COULDNT_CONNECT: + ctx->completion_handler( + HttpResponseOutcome(Core::Error("ConnectionError", err_msg))); + break; + case CURLE_PEER_FAILED_VERIFICATION: + // Server certificate verification failed. + // High-confidence signal of DNS hijacking for + // TencentCloud domains. + ctx->completion_handler( + HttpResponseOutcome(Core::Error("SSLError", err_msg))); + break; + default: + // CURLE_OPERATION_TIMEDOUT, CURLE_SSL_CONNECT_ERROR, + // CURLE_SSL_CERTPROBLEM, CURLE_SSL_CIPHER and other + // errors fall here. They are mostly client-side + // issues and cannot be fixed by region/TLD switch. + ctx->completion_handler( + HttpResponseOutcome(Core::Error("NetworkError", err_msg))); + break; + } + } + else { int64_t response_code = 0; curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response_code); @@ -495,7 +563,7 @@ size_t HttpClient::CurlReadHeader(char* ptr, size_t size, size_t nitems, void* u if (colon_pos == std::string::npos) { // invalid format - return nitems * size;; + return nitems * size; } const auto key = line.substr(0, colon_pos); diff --git a/core/src/profile/ClientProfile.cpp b/core/src/profile/ClientProfile.cpp index 056075d316..c46a53ed62 100644 --- a/core/src/profile/ClientProfile.cpp +++ b/core/src/profile/ClientProfile.cpp @@ -43,3 +43,23 @@ bool ClientProfile::IsUnsignedPayload() { return m_unsignedPayload; } + +void ClientProfile::SetDisableRegionBreaker(bool disabled) +{ + m_disableRegionBreaker = disabled; +} + +bool ClientProfile::GetDisableRegionBreaker() const +{ + return m_disableRegionBreaker; +} + +void ClientProfile::SetRegionBreakerProfile(const RegionBreakerProfile &profile) +{ + m_regionBreakerProfile = profile; +} + +RegionBreakerProfile ClientProfile::GetRegionBreakerProfile() const +{ + return m_regionBreakerProfile; +} diff --git a/example/cvm/v20170312/CMakeLists.txt b/example/cvm/v20170312/CMakeLists.txt index 46deadbddd..2886e52c5c 100644 --- a/example/cvm/v20170312/CMakeLists.txt +++ b/example/cvm/v20170312/CMakeLists.txt @@ -5,7 +5,10 @@ set(CMAKE_CXX_STANDARD 11) add_executable(DescribeInstances DescribeInstances.cpp) add_executable(DescribeInstancesAsync DescribeInstancesAsync.cpp) -target_link_libraries(DescribeInstances cvm core) -target_link_libraries(DescribeInstancesAsync cvm core) +add_executable(DomainFailover DomainFailover.cpp) +target_link_libraries(DescribeInstances tencentcloud-sdk-cpp-cvm tencentcloud-sdk-cpp-core) +target_link_libraries(DescribeInstancesAsync tencentcloud-sdk-cpp-cvm tencentcloud-sdk-cpp-core) +target_link_libraries(DomainFailover tencentcloud-sdk-cpp-cvm tencentcloud-sdk-cpp-core) target_include_directories(DescribeInstances PRIVATE ../../../core/include ../../../cvm/include) -target_include_directories(DescribeInstancesAsync PRIVATE ../../../core/include ../../../cvm/include) \ No newline at end of file +target_include_directories(DescribeInstancesAsync PRIVATE ../../../core/include ../../../cvm/include) +target_include_directories(DomainFailover PRIVATE ../../../core/include ../../../cvm/include) diff --git a/example/cvm/v20170312/DomainFailover.cpp b/example/cvm/v20170312/DomainFailover.cpp new file mode 100644 index 0000000000..5cdfc0a50a --- /dev/null +++ b/example/cvm/v20170312/DomainFailover.cpp @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +/// +/// This example demonstrates region-level domain failover (aligned with +/// Python SDK's region_breaker) in the C++ SDK. +/// +/// When the primary endpoint keeps failing, a circuit breaker drives +/// automatic switching. There are two mutually exclusive fallback modes: +/// Mode A (BackupEndpoint configured): +/// primary -> user-configured BackupEndpoint (no TLD fallback) +/// Mode B (no BackupEndpoint, default): +/// primary -> tencentcloudapi.com.cn -> tencentcloudapi.cn (TLD ring) +/// +/// Run with: TENCENTCLOUD_SECRET_ID=xxx TENCENTCLOUD_SECRET_KEY=yyy ./DomainFailover +/// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace TencentCloud; +using namespace TencentCloud::Cvm::V20170312; +using namespace TencentCloud::Cvm::V20170312::Model; +using namespace std; + +static void PrintOutcome(CvmClient::DescribeInstancesOutcome outcome) { + if (!outcome.IsSuccess()) { + cout << " Error: " << outcome.GetError().PrintAll() << endl; + } else { + auto rsp = outcome.GetResult(); + cout << " RequestId=" << rsp.GetRequestId() + << " TotalCount=" << rsp.GetTotalCount() << endl; + } + cout << endl; +} + +/// Example 1: Default behavior - region failover is ENABLED by default. +/// The SDK uses circuit breakers to automatically fall back to alternate +/// endpoints when the primary endpoint keeps failing. +void DefaultEnabled() { + cout << "=== Example 1: Default (Region Failover Enabled) ===" << endl; + + const char* sid = getenv("TENCENTCLOUD_SECRET_ID"); + const char* skey = getenv("TENCENTCLOUD_SECRET_KEY"); + Credential cred(sid ? sid : "", skey ? skey : ""); + + CvmClient client(cred, "ap-guangzhou"); + + DescribeInstancesRequest req; + req.SetOffset(0); + req.SetLimit(5); + PrintOutcome(client.DescribeInstances(req)); +} + +/// Example 2: Enable region failover. +/// Without an explicit BackupEndpoint, the BackupEndpoint layer is +/// skipped; the SDK falls back directly to .com.cn then .cn. +void EnableWithoutBackupEndpoint() { + cout << "=== Example 2: Region Failover (no BackupEndpoint, TLD only) ===" << endl; + + const char* sid = getenv("TENCENTCLOUD_SECRET_ID"); + const char* skey = getenv("TENCENTCLOUD_SECRET_KEY"); + Credential cred(sid ? sid : "", skey ? skey : ""); + + ClientProfile clientProfile; + clientProfile.SetDisableRegionBreaker(false); // Enable failover + // RegionBreakerProfile left with default-empty BackupEndpoint + + CvmClient client(cred, "ap-guangzhou", clientProfile); + + DescribeInstancesRequest req; + req.SetOffset(0); + req.SetLimit(5); + PrintOutcome(client.DescribeInstances(req)); +} + +/// Example 3: Custom RegionBreakerProfile with multiple requests. +/// +/// Uses a fabricated sub-domain as primary (will fail with SSLError) and a +/// real BackupEndpoint. After enough failures trip the breaker (max_fail_num=3), +/// subsequent requests are routed to the BackupEndpoint. +/// +/// Expected output: +/// Requests 1~3: SSLError (primary endpoint cert mismatch) +/// Request 4+: Success (failover to backup endpoint) +void EnableWithCustomProfile() { + cout << "=== Example 3: Custom RegionBreakerProfile ===" << endl; + cout << " Primary endpoint: cvm.does-not-exist.tencentcloudapi.com (SSL will fail)" << endl; + cout << " Backup endpoint: ap-beijing.tencentcloudapi.com (will succeed)" << endl; + cout << endl; + + const char* sid = getenv("TENCENTCLOUD_SECRET_ID"); + const char* skey = getenv("TENCENTCLOUD_SECRET_KEY"); + if (!sid || !skey) { + cout << " [SKIP] Set TENCENTCLOUD_SECRET_ID and TENCENTCLOUD_SECRET_KEY to run this example." << endl; + cout << endl; + return; + } + Credential cred(sid, skey); + + RegionBreakerProfile rb( + /*backup_endpoint=*/ "ap-beijing.tencentcloudapi.com", + /*max_fail_num=*/ 3, + /*max_fail_percent=*/0.5, + /*window_interval=*/ 60, + /*timeout=*/ 30, + /*max_requests=*/ 3); + + ClientProfile clientProfile; + clientProfile.SetDisableRegionBreaker(false); + clientProfile.SetRegionBreakerProfile(rb); + + HttpProfile httpProfile; + httpProfile.SetEndpoint("cvm.does-not-exist.tencentcloudapi.com"); + httpProfile.SetConnectTimeout(3); + httpProfile.SetReqTimeout(5); + clientProfile.SetHttpProfile(httpProfile); + + CvmClient client(cred, "ap-guangzhou", clientProfile); + + DescribeInstancesRequest req; + req.SetOffset(0); + req.SetLimit(1); + + const int kTotalRequests = 6; + int fail_count = 0; + int success_count = 0; + + for (int i = 1; i <= kTotalRequests; ++i) { + cout << " Request #" << i << ": "; + auto outcome = client.DescribeInstances(req); + if (!outcome.IsSuccess()) { + ++fail_count; + cout << "FAILED - " << outcome.GetError().GetErrorCode() + << ": " << outcome.GetError().GetErrorMessage() << endl; + } else { + ++success_count; + cout << "SUCCESS - RequestId=" << outcome.GetResult().GetRequestId() << endl; + } + } + + cout << endl; + cout << " Summary: " << fail_count << " failed, " + << success_count << " succeeded (failover triggered after " + << fail_count << " failures)" << endl; + cout << endl; +} + +/// Example 4: Demonstrate actual failover in action. +/// +/// Sets the primary endpoint to a domain that passes the SDK's +/// IsTencentCloudDomain() check (ends with ".tencentcloudapi.com") +/// but uses a fabricated sub-domain that will fail with SSLError +/// (CURLE_PEER_FAILED_VERIFICATION) because the wildcard DNS record +/// resolves it to a server whose certificate SAN does not match. +/// +/// SSLError is one of the failover-triggering error codes (it is a +/// strong signal of DNS hijacking for legitimate TencentCloud domains). +/// The circuit breaker trips after consecutive SSLError failures, and +/// subsequent requests are routed to the BackupEndpoint (a real domain). +/// +/// Expected output: +/// Requests 1~5: SSLError (certificate SAN mismatch on fabricated domain) +/// Request 6+: Success (failover to backup endpoint) +void DemonstrateActualFailover() { + cout << "=== Example 4: Actual Failover Demonstration ===" << endl; + cout << " Primary endpoint: cvm.does-not-exist.tencentcloudapi.com (SSL will fail)" << endl; + cout << " Backup endpoint: ap-guangzhou.tencentcloudapi.com (will succeed)" << endl; + cout << endl; + + const char* sid = getenv("TENCENTCLOUD_SECRET_ID"); + const char* skey = getenv("TENCENTCLOUD_SECRET_KEY"); + if (!sid || !skey) { + cout << " [SKIP] Set TENCENTCLOUD_SECRET_ID and TENCENTCLOUD_SECRET_KEY to run this example." << endl; + cout << endl; + return; + } + Credential cred(sid, skey); + + // Use the default breaker thresholds (max_fail_num=5): the breaker + // trips after 5 consecutive SSLError failures on the primary. + RegionBreakerProfile rb( + /*backup_endpoint=*/ "ap-guangzhou.tencentcloudapi.com", + /*max_fail_num=*/ 5, + /*max_fail_percent=*/0.75, + /*window_interval=*/ 300, + /*timeout=*/ 60, + /*max_requests=*/ 3); + + ClientProfile clientProfile; + clientProfile.SetDisableRegionBreaker(false); + clientProfile.SetRegionBreakerProfile(rb); + + HttpProfile httpProfile; + // Fabricated sub-domain under tencentcloudapi.com: + // - DNS resolves (wildcard A record) -> passes IsTencentCloudDomain() + // - SSL certificate SAN does not match -> CURLE_PEER_FAILED_VERIFICATION + // - SDK maps to "SSLError" -> triggers failover + httpProfile.SetEndpoint("cvm.does-not-exist.tencentcloudapi.com"); + httpProfile.SetConnectTimeout(3); + httpProfile.SetReqTimeout(5); + clientProfile.SetHttpProfile(httpProfile); + + CvmClient client(cred, "ap-guangzhou", clientProfile); + + DescribeInstancesRequest req; + req.SetOffset(0); + req.SetLimit(1); + + // Send 10 requests to observe failover behavior. + // + // How to tell that the endpoint has switched from the output: + // + // - SSLError with message containing "does-not-exist" means the + // request is still hitting the PRIMARY endpoint (SSL cert SAN + // mismatch on the fabricated sub-domain). + // + // - Once the output changes from FAILED to SUCCESS, the circuit + // breaker has tripped (Closed -> Open) and the SDK is now + // routing requests to the BACKUP endpoint + // (cvm.ap-guangzhou.tencentcloudapi.com). + // + // - The transition point (last FAILED -> first SUCCESS) is + // exactly where the domain switch happened. + // + // For deeper debugging, enable curl verbose mode or add a log line + // in AbstractClient::DoRequest() after SelectEndpoint() returns: + // std::cerr << "[Failover] endpoint=" << decision.host << std::endl; + const int kTotalRequests = 10; + int fail_count = 0; + int success_count = 0; + + for (int i = 1; i <= kTotalRequests; ++i) { + cout << " Request #" << i << ": "; + auto outcome = client.DescribeInstances(req); + if (!outcome.IsSuccess()) { + ++fail_count; + cout << "FAILED - " << outcome.GetError().GetErrorCode() + << ": " << outcome.GetError().GetErrorMessage() << endl; + } else { + ++success_count; + cout << "SUCCESS - RequestId=" << outcome.GetResult().GetRequestId() << endl; + } + } + + cout << endl; + cout << " Summary: " << fail_count << " failed, " + << success_count << " succeeded (failover triggered after " + << fail_count << " failures)" << endl; + cout << endl; +} + +/// Example 5: Failover WITHOUT BackupEndpoint (TLD-only fallback). +/// +/// With no BackupEndpoint, fallback goes through the TLD ring. The +/// primary cvm.does-not-exist.tencentcloudapi.com carries a region-like +/// segment ("does-not-exist") which is DROPPED on fallback, so the +/// breaker trips and the SDK switches to cvm.tencentcloudapi.com.cn. +/// +/// Expected output: +/// Requests 1~5: SSLError (primary cert mismatch on fabricated domain) +/// Request 6+: Success (failover to cvm.tencentcloudapi.com.cn) +void DemonstrateFailoverWithoutBackup() { + cout << "=== Example 5: Failover Without BackupEndpoint (TLD fallback) ===" << endl; + cout << " Primary endpoint: cvm.does-not-exist.tencentcloudapi.com (will fail)" << endl; + cout << " BackupEndpoint: (empty - skipped)" << endl; + cout << " Expected target: cvm.tencentcloudapi.com.cn" << endl; + cout << endl; + + const char* sid = getenv("TENCENTCLOUD_SECRET_ID"); + const char* skey = getenv("TENCENTCLOUD_SECRET_KEY"); + if (!sid || !skey) { + cout << " [SKIP] Set TENCENTCLOUD_SECRET_ID and TENCENTCLOUD_SECRET_KEY to run this example." << endl; + cout << endl; + return; + } + Credential cred(sid, skey); + + // No BackupEndpoint: leave RegionBreakerProfile with default empty string. + RegionBreakerProfile rb( + /*backup_endpoint=*/ "", + /*max_fail_num=*/ 5, + /*max_fail_percent=*/0.75, + /*window_interval=*/ 300, + /*timeout=*/ 60, + /*max_requests=*/ 3); + + ClientProfile clientProfile; + clientProfile.SetDisableRegionBreaker(false); + clientProfile.SetRegionBreakerProfile(rb); + + HttpProfile httpProfile; + // Fabricated two-level sub-domain: SSL cert SAN mismatch triggers + // SSLError, which opens the breaker and drives TLD fallback. + httpProfile.SetEndpoint("cvm.does-not-exist.tencentcloudapi.com"); + httpProfile.SetConnectTimeout(3); + httpProfile.SetReqTimeout(5); + clientProfile.SetHttpProfile(httpProfile); + + CvmClient client(cred, "ap-guangzhou", clientProfile); + + DescribeInstancesRequest req; + req.SetOffset(0); + req.SetLimit(1); + + // After max_fail_num consecutive SSLError failures on the primary, the + // breaker opens. With no BackupEndpoint, the region-like segment is + // dropped and the SDK falls back through the TLD ring: + // cvm.does-not-exist.tencentcloudapi.com -> cvm.tencentcloudapi.com.cn + const int kTotalRequests = 10; + int fail_count = 0; + int success_count = 0; + + for (int i = 1; i <= kTotalRequests; ++i) { + cout << " Request #" << i << ": "; + auto outcome = client.DescribeInstances(req); + if (!outcome.IsSuccess()) { + ++fail_count; + cout << "FAILED - " << outcome.GetError().GetErrorCode() + << ": " << outcome.GetError().GetErrorMessage() << endl; + } else { + ++success_count; + cout << "SUCCESS - RequestId=" << outcome.GetResult().GetRequestId() << endl; + } + } + + cout << endl; + cout << " Summary: " << fail_count << " failed, " + << success_count << " succeeded" << endl; + cout << " (Without BackupEndpoint, failover goes to .com.cn TLD)" << endl; + cout << endl; +} + +int main() { + TencentCloud::InitAPI(); + + DefaultEnabled(); + EnableWithoutBackupEndpoint(); + EnableWithCustomProfile(); + DemonstrateActualFailover(); + DemonstrateFailoverWithoutBackup(); + + TencentCloud::ShutdownAPI(); + return 0; +} diff --git a/test/function_test/core/CMakeLists.txt b/test/function_test/core/CMakeLists.txt index 7c3ebce9a6..d79265c488 100644 --- a/test/function_test/core/CMakeLists.txt +++ b/test/function_test/core/CMakeLists.txt @@ -32,7 +32,11 @@ add_executable(core_ft Core_Http_HttpResponse_Ft.cpp Core_Profile_HttpProfile_Ft.cpp Core_Profile_ClientProfile_Ft.cpp + Core_Profile_DomainFailover_Ft.cpp Core_Http_ResolveIp_Ft.cpp + Core_CircuitBreaker_Ft.cpp + Core_DomainFailoverManager_Ft.cpp + Core_AbstractClient_Breaker_Ft.cpp Main.cpp) target_link_libraries(core_ft tencentcloud-sdk-cpp-cbs tencentcloud-sdk-cpp-cvm tencentcloud-sdk-cpp-core) target_link_libraries(core_ft gtest gmock_main -lpthread -lm) diff --git a/test/function_test/core/Cbs_DescribeDisks_SetCA_Ft.cpp b/test/function_test/core/Cbs_DescribeDisks_SetCA_Ft.cpp index fd3f3102fd..dd2cfc8e96 100644 --- a/test/function_test/core/Cbs_DescribeDisks_SetCA_Ft.cpp +++ b/test/function_test/core/Cbs_DescribeDisks_SetCA_Ft.cpp @@ -171,6 +171,10 @@ httpProfile.SetEndpoint("cbs.ap-guangzhou.tencentcloudapi.com"); httpProfile.SetReqTimeout(5); httpProfile.SetCaPath("/tmp/nodir/"); + // Also override CAINFO so that libcurl's compile-time default + // CA bundle (if any) is disabled. Both CA sources must be + // invalid for the SSL handshake to reliably fail. + httpProfile.SetCaInfo("/tmp/noexist.pem"); ClientProfile clientProfile = ClientProfile(httpProfile); diff --git a/test/function_test/core/Core_AbstractClient_Breaker_Ft.cpp b/test/function_test/core/Core_AbstractClient_Breaker_Ft.cpp new file mode 100644 index 0000000000..f1d339d467 --- /dev/null +++ b/test/function_test/core/Core_AbstractClient_Breaker_Ft.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#include +#include +#include +#include + +#include +#include +#include + +using namespace TencentCloud; +using namespace std; + +namespace { + +// Exposes protected breaker helpers for deterministic testing. +class TestableClient : public AbstractClient { + public: + explicit TestableClient(const ClientProfile &p) + : AbstractClient("cvm.tencentcloudapi.com", "2017-03-12", + Credential("id", "key"), "ap-guangzhou", p) {} + using AbstractClient::BreakerFor; + using AbstractClient::SelectEndpoint; + using AbstractClient::ReportResult; + using AbstractClient::EndpointDecision; +}; + +// max_fail_num=1 -> a single failure trips the breaker. +ClientProfile MakeProfile(const string &backup, bool disabled = false) { + RegionBreakerProfile rb(backup, 1, 0.0, 300, 60, 1); + ClientProfile cp; + cp.SetDisableRegionBreaker(disabled); + cp.SetRegionBreakerProfile(rb); + return cp; +} + +void Trip(TestableClient &c, const string &origin, const string &host) { + std::shared_ptr b = c.BreakerFor(origin, host); + ASSERT_TRUE(b->Allow()); + b->Report(false); + ASSERT_EQ(b->GetState(), CircuitBreaker::State::kOpen); +} + +const char *kP = "cvm.tencentcloudapi.com"; +const char *kFb1 = "cvm.tencentcloudapi.com.cn"; +const char *kFb2 = "cvm.tencentcloudapi.cn"; // bottom (no breaker) + +} // namespace + +TEST(AbstractClientBreakerTest, BreakerForReusesSameInstance) { + TestableClient c(MakeProfile("")); + EXPECT_EQ(c.BreakerFor(kP, kP), c.BreakerFor(kP, kP)); +} + +TEST(AbstractClientBreakerTest, BreakerStateIsolatedAcrossOriginHosts) { + TestableClient c(MakeProfile("")); + Trip(c, kP, kP); + std::shared_ptr cls = + c.BreakerFor("cls.tencentcloudapi.com", "cls.tencentcloudapi.com"); + EXPECT_EQ(cls->GetState(), CircuitBreaker::State::kClosed); +} + +TEST(AbstractClientBreakerTest, SelectsPrimaryWhenAllClosed) { + TestableClient c(MakeProfile("")); + TestableClient::EndpointDecision d = c.SelectEndpoint(kP); + EXPECT_EQ(d.host, kP); + EXPECT_NE(d.breaker, nullptr); +} + +TEST(AbstractClientBreakerTest, DescendsToNextCandidateInOrder) { + TestableClient c(MakeProfile("")); + Trip(c, kP, kP); + TestableClient::EndpointDecision d = c.SelectEndpoint(kP); + EXPECT_EQ(d.host, kFb1); + EXPECT_NE(d.breaker, nullptr); +} + +TEST(AbstractClientBreakerTest, FrontCandidatesOpenFallsToBottomNoReport) { + TestableClient c(MakeProfile("")); + Trip(c, kP, kP); + Trip(c, kP, kFb1); // kFb2 is the bottom, has no breaker + TestableClient::EndpointDecision d = c.SelectEndpoint(kP); + EXPECT_EQ(d.host, kFb2); + EXPECT_EQ(d.breaker, nullptr); +} + +TEST(AbstractClientBreakerTest, DisabledBypass) { + TestableClient c(MakeProfile("", /*disabled=*/true)); + TestableClient::EndpointDecision d = c.SelectEndpoint(kP); + EXPECT_EQ(d.host, kP); + EXPECT_EQ(d.breaker, nullptr); +} + +TEST(AbstractClientBreakerTest, NonTencentCloudBypass) { + TestableClient c(MakeProfile("")); + TestableClient::EndpointDecision d = c.SelectEndpoint("my-proxy.corp.com"); + EXPECT_EQ(d.host, "my-proxy.corp.com"); + EXPECT_EQ(d.breaker, nullptr); +} + +TEST(AbstractClientBreakerTest, RegionPrimaryNowHasFallbackWithBreaker) { + // CHANGED BEHAVIOR: region is no longer suppressed. A regional primary + // with no backup now produces 3 candidates (primary + 2 TLD fallbacks), + // so SelectEndpoint assigns a breaker to the primary (Closed -> Allow). + TestableClient c(MakeProfile("")); + TestableClient::EndpointDecision d = + c.SelectEndpoint("cvm.ap-shanghai.tencentcloudapi.com"); + EXPECT_EQ(d.host, "cvm.ap-shanghai.tencentcloudapi.com"); + EXPECT_NE(d.breaker, nullptr); // breaker assigned (Closed state) +} + +TEST(AbstractClientBreakerTest, ReportNullBreakerIsNoop) { + TestableClient c(MakeProfile("")); + c.ReportResult(nullptr, false); + c.ReportResult(nullptr, true); +} diff --git a/test/function_test/core/Core_CircuitBreaker_Ft.cpp b/test/function_test/core/Core_CircuitBreaker_Ft.cpp new file mode 100644 index 0000000000..ea05dfd986 --- /dev/null +++ b/test/function_test/core/Core_CircuitBreaker_Ft.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#include +#include + +#include +#include +#include + +using namespace TencentCloud; + +static RegionBreakerProfile MakeProfile(int max_fail_num, + double max_fail_percent, + int window_interval, + int timeout, + int max_requests) { + return RegionBreakerProfile("ap-guangzhou.tencentcloudapi.com", + max_fail_num, max_fail_percent, + window_interval, timeout, max_requests); +} + +// Drives |n| Allow/Report round-trips with the given outcome. Assumes +// every Allow() is granted (caller controls state). +static void DrainN(CircuitBreaker &cb, int n, bool success) { + for (int i = 0; i < n; ++i) { + ASSERT_TRUE(cb.Allow()); + cb.Report(success); + } +} + +TEST(CircuitBreakerTest, DefaultStateClosed) { + CircuitBreaker cb; + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kClosed); + EXPECT_TRUE(cb.Allow()); + cb.Report(true); // pair with the Allow above +} + +TEST(CircuitBreakerTest, StayClosedOnSuccess) { + CircuitBreaker cb; + DrainN(cb, 100, true); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kClosed); +} + +TEST(CircuitBreakerTest, TripOnFailNumAndFailRate) { + CircuitBreaker cb(MakeProfile(3, 0.75, 300, 60, 3)); + DrainN(cb, 3, false); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kOpen); +} + +TEST(CircuitBreakerTest, DoesNotTripWhenRateTooLow) { + CircuitBreaker cb(MakeProfile(3, 0.9, 300, 60, 3)); + DrainN(cb, 2, false); + DrainN(cb, 2, true); + DrainN(cb, 1, false); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kClosed); +} + +TEST(CircuitBreakerTest, DoesNotTripWhenCountTooLow) { + CircuitBreaker cb(MakeProfile(5, 0.5, 300, 60, 3)); + DrainN(cb, 4, false); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kClosed); + DrainN(cb, 1, false); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kOpen); +} + +TEST(CircuitBreakerTest, TripOnConsecutiveFailuresReachesMaxFailNum) { + // Even with an unreachable rate threshold, consecutive failures + // reaching max_fail_num still trip the breaker via the + // consecutive-failure branch. Here max_fail_num=3, so 3 consecutive + // failures are enough (no need to wait for the rate branch). + CircuitBreaker cb(MakeProfile(3, 1.1, 300, 60, 3)); + DrainN(cb, 3, false); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kOpen); +} + +TEST(CircuitBreakerTest, ConsecutiveFailuresUseConfiguredMaxFailNum) { + // Verify that the consecutive-failure threshold follows the user's + // max_fail_num, not a hardcoded value. With max_fail_num=7 the + // breaker should NOT trip after 5 consecutive failures (the old + // hardcoded default), but SHOULD trip after 7. + CircuitBreaker cb(MakeProfile(7, 1.1, 300, 60, 3)); + DrainN(cb, 5, false); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kClosed); + DrainN(cb, 2, false); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kOpen); +} + +TEST(CircuitBreakerTest, ConsecutiveFailuresOverrideRateBranch) { + // When failures are interleaved with successes, the rate branch + // may not trigger, but consecutive failures should still work. + // max_fail_num=3, max_fail_percent=0.9 (90% failure rate needed). + CircuitBreaker cb(MakeProfile(3, 0.9, 300, 60, 3)); + // 2 failures, 1 success, 3 consecutive failures: + // total=6, failures=5, rate=83% < 90% → rate branch NOT triggered. + // But consecutive=3 >= max_fail_num=3 → consecutive branch triggers. + ASSERT_TRUE(cb.Allow()); cb.Report(false); // consec=1, total=1, fail=1 + ASSERT_TRUE(cb.Allow()); cb.Report(false); // consec=2, total=2, fail=2 + ASSERT_TRUE(cb.Allow()); cb.Report(true); // consec=0, total=3, fail=2 + ASSERT_TRUE(cb.Allow()); cb.Report(false); // consec=1, total=4, fail=3 + ASSERT_TRUE(cb.Allow()); cb.Report(false); // consec=2, total=5, fail=4 + ASSERT_TRUE(cb.Allow()); cb.Report(false); // consec=3, total=6, fail=5 + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kOpen); +} + +TEST(CircuitBreakerTest, OpenStateDisallowsRequests) { + CircuitBreaker cb(MakeProfile(3, 0.75, 300, 60, 3)); + DrainN(cb, 3, false); + EXPECT_FALSE(cb.Allow()); +} + +TEST(CircuitBreakerTest, TransitionsToHalfOpenAfterTimeout) { + CircuitBreaker cb(MakeProfile(3, 0.75, 300, 1, 3)); + DrainN(cb, 3, false); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kHalfOpen); + EXPECT_TRUE(cb.Allow()); + cb.Report(true); // pair with Allow +} + +TEST(CircuitBreakerTest, HalfOpenClosesAfterExceedingMaxRequestsSuccesses) { + // Requires (total - failures) >= max_requests to close. With + // max_requests = 3 that is the 3rd successful probe. + CircuitBreaker cb(MakeProfile(3, 0.75, 300, 1, 3)); + DrainN(cb, 3, false); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + + EXPECT_TRUE(cb.Allow()); cb.Report(true); // 1 + EXPECT_TRUE(cb.Allow()); cb.Report(true); // 2 + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kHalfOpen); + EXPECT_TRUE(cb.Allow()); cb.Report(true); // 3 (>= max_requests) -> Closed + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kClosed); +} + +TEST(CircuitBreakerTest, HalfOpenReopensOnAnyFailure) { + CircuitBreaker cb(MakeProfile(3, 0.75, 300, 1, 3)); + DrainN(cb, 3, false); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + EXPECT_TRUE(cb.Allow()); + cb.Report(false); + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kOpen); +} + +TEST(CircuitBreakerTest, ClosedWindowExpiryResetsCounters) { + // After |window_interval| the Closed-state counters reset. Earlier + // failures must not contribute to the trip condition. + CircuitBreaker cb(MakeProfile(3, 0.75, /*window_interval=*/1, 60, 3)); + DrainN(cb, 2, false); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + // Next Allow() lazily advances the state and resets counters. + DrainN(cb, 2, false); // Only 2 failures in the new window. + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kClosed); +} + +// ===== HalfOpen concurrent-probe rate limit ===== + +TEST(CircuitBreakerTest, HalfOpenRateLimitsConcurrentProbes) { + // max_requests = 2: up to 2 probes may be in-flight at once in + // HalfOpen. A 3rd concurrent Allow() without any Report in between + // must be rejected. + CircuitBreaker cb(MakeProfile(3, 0.75, 300, 1, 2)); + DrainN(cb, 3, false); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + + EXPECT_TRUE(cb.Allow()); // slot 1 + EXPECT_TRUE(cb.Allow()); // slot 2 + EXPECT_FALSE(cb.Allow()); // budget exhausted + EXPECT_EQ(cb.GetState(), CircuitBreaker::State::kHalfOpen); + + // Release one slot via Report; budget becomes available again. + cb.Report(true); + EXPECT_TRUE(cb.Allow()); + cb.Report(true); // release + cb.Report(true); // release the 1st probe too +} diff --git a/test/function_test/core/Core_DomainFailoverManager_Ft.cpp b/test/function_test/core/Core_DomainFailoverManager_Ft.cpp new file mode 100644 index 0000000000..c248fb2eab --- /dev/null +++ b/test/function_test/core/Core_DomainFailoverManager_Ft.cpp @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#include + +#include +#include + +using namespace TencentCloud; +using namespace std; + +// ===== Helper methods ===== + +TEST(DomainFailoverManagerTest, IsTencentCloudDomain) { + EXPECT_TRUE(DomainFailoverManager::IsTencentCloudDomain("cvm.tencentcloudapi.com")); + EXPECT_TRUE(DomainFailoverManager::IsTencentCloudDomain("cvm.tencentcloudapi.com.cn")); + EXPECT_TRUE(DomainFailoverManager::IsTencentCloudDomain("cvm.tencentcloudapi.cn")); + EXPECT_FALSE(DomainFailoverManager::IsTencentCloudDomain("my-proxy.corp.com")); + EXPECT_FALSE(DomainFailoverManager::IsTencentCloudDomain("tencentcloud.com")); + // Suffix match must require a leading dot to avoid misidentifying + // look-alike / typo domains. + EXPECT_FALSE(DomainFailoverManager::IsTencentCloudDomain("tencentcloudapi.comm")); + EXPECT_FALSE(DomainFailoverManager::IsTencentCloudDomain("tencentcloudapi.company.example.com")); + EXPECT_FALSE(DomainFailoverManager::IsTencentCloudDomain("faketencentcloudapi.com")); +} + +TEST(DomainFailoverManagerTest, IsTencentCloudDomainCaseInsensitive) { + // RFC 4343: domain names are case-insensitive. + EXPECT_TRUE(DomainFailoverManager::IsTencentCloudDomain("CVM.TencentCloudApi.COM")); + EXPECT_TRUE(DomainFailoverManager::IsTencentCloudDomain("CVM.TENCENTCLOUDAPI.COM.CN")); + EXPECT_TRUE(DomainFailoverManager::IsTencentCloudDomain("cvm.TencentCloudApi.Cn")); +} + +TEST(DomainFailoverManagerTest, ExtractTldCaseInsensitive) { + EXPECT_EQ(DomainFailoverManager::ExtractTld("CVM.TencentCloudApi.COM"), + "tencentcloudapi.com"); + EXPECT_EQ(DomainFailoverManager::ExtractTld("cvm.TENCENTCLOUDAPI.COM.CN"), + "tencentcloudapi.com.cn"); +} + +TEST(DomainFailoverManagerTest, ExtractTld) { + EXPECT_EQ(DomainFailoverManager::ExtractTld("cvm.tencentcloudapi.com"), + "tencentcloudapi.com"); + EXPECT_EQ(DomainFailoverManager::ExtractTld("cvm.tencentcloudapi.com.cn"), + "tencentcloudapi.com.cn"); + EXPECT_EQ(DomainFailoverManager::ExtractTld("cvm.tencentcloudapi.cn"), + "tencentcloudapi.cn"); + EXPECT_EQ(DomainFailoverManager::ExtractTld("my-proxy.corp.com"), ""); + EXPECT_EQ(DomainFailoverManager::ExtractTld("tencentcloudapi.comm"), ""); + EXPECT_EQ(DomainFailoverManager::ExtractTld("tencentcloudapi.company.example.com"), ""); +} + +// ===== BuildCandidates ===== + +TEST(BuildCandidatesTest, ModeB_ComNoRegion) { + auto c = DomainFailoverManager::BuildCandidates("cvm.tencentcloudapi.com", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[0], "cvm.tencentcloudapi.com"); + EXPECT_EQ(c[1], "cvm.tencentcloudapi.com.cn"); + EXPECT_EQ(c[2], "cvm.tencentcloudapi.cn"); +} + +TEST(BuildCandidatesTest, ModeB_ComCnNoRegion) { + auto c = DomainFailoverManager::BuildCandidates("cvm.tencentcloudapi.com.cn", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[0], "cvm.tencentcloudapi.com.cn"); + EXPECT_EQ(c[1], "cvm.tencentcloudapi.cn"); + EXPECT_EQ(c[2], "cvm.tencentcloudapi.com"); +} + +TEST(BuildCandidatesTest, ModeB_CnNoRegion) { + auto c = DomainFailoverManager::BuildCandidates("cvm.tencentcloudapi.cn", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[0], "cvm.tencentcloudapi.cn"); + EXPECT_EQ(c[1], "cvm.tencentcloudapi.com"); + EXPECT_EQ(c[2], "cvm.tencentcloudapi.com.cn"); +} + +TEST(BuildCandidatesTest, ModeB_AiDomainPreservesMiddle) { + auto c = DomainFailoverManager::BuildCandidates("hunyuan.ai.tencentcloudapi.com", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[0], "hunyuan.ai.tencentcloudapi.com"); + EXPECT_EQ(c[1], "hunyuan.ai.tencentcloudapi.com.cn"); + EXPECT_EQ(c[2], "hunyuan.ai.tencentcloudapi.cn"); +} + +TEST(BuildCandidatesTest, ModeB_InternalDomainPreservesMiddle) { + auto c = DomainFailoverManager::BuildCandidates("cvm.internal.tencentcloudapi.com", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[1], "cvm.internal.tencentcloudapi.com.cn"); + EXPECT_EQ(c[2], "cvm.internal.tencentcloudapi.cn"); +} + +TEST(BuildCandidatesTest, ModeB_IntlModifierDropped) { + // intl is recognized but DROPPED on fallback. + auto c = DomainFailoverManager::BuildCandidates("cvm.intl.tencentcloudapi.com", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[0], "cvm.intl.tencentcloudapi.com"); + EXPECT_EQ(c[1], "cvm.tencentcloudapi.com.cn"); + EXPECT_EQ(c[2], "cvm.tencentcloudapi.cn"); +} + +TEST(BuildCandidatesTest, ModeB_RegionNowFallsBackDroppingRegion) { + // CHANGED BEHAVIOR: region is no longer suppressed; it falls back + // dropping the region segment. + auto c = DomainFailoverManager::BuildCandidates( + "cvm.ap-shanghai.tencentcloudapi.com", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[0], "cvm.ap-shanghai.tencentcloudapi.com"); + EXPECT_EQ(c[1], "cvm.tencentcloudapi.com.cn"); + EXPECT_EQ(c[2], "cvm.tencentcloudapi.cn"); +} + +TEST(BuildCandidatesTest, ModeB_AiRegionKeepsAiDropsRegion) { + auto c = DomainFailoverManager::BuildCandidates( + "cvm.ai.ap-shanghai.tencentcloudapi.com", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[1], "cvm.ai.tencentcloudapi.com.cn"); + EXPECT_EQ(c[2], "cvm.ai.tencentcloudapi.cn"); +} + +TEST(BuildCandidatesTest, ModeB_IntlRegionBothDropped) { + auto c = DomainFailoverManager::BuildCandidates( + "cvm.intl.ap-guangzhou.tencentcloudapi.com", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[1], "cvm.tencentcloudapi.com.cn"); + EXPECT_EQ(c[2], "cvm.tencentcloudapi.cn"); +} + +TEST(BuildCandidatesTest, ModeB_ExtremeStackTakesAiDropsRest) { + auto c = DomainFailoverManager::BuildCandidates( + "cvm.ai.internal.intl.ap-guangzhou.tencentcloudapi.com", ""); + ASSERT_EQ(c.size(), 3u); + EXPECT_EQ(c[1], "cvm.ai.tencentcloudapi.com.cn"); + EXPECT_EQ(c[2], "cvm.ai.tencentcloudapi.cn"); +} + +TEST(BuildCandidatesTest, ModeA_RegionPrimaryWithBareBackup) { + auto c = DomainFailoverManager::BuildCandidates( + "cvm.ap-shanghai.tencentcloudapi.com", "ap-guangzhou.tencentcloudapi.com"); + ASSERT_EQ(c.size(), 2u); + EXPECT_EQ(c[0], "cvm.ap-shanghai.tencentcloudapi.com"); + EXPECT_EQ(c[1], "cvm.ap-guangzhou.tencentcloudapi.com"); +} + +TEST(BuildCandidatesTest, ModeA_AiRegionBackupDropsAiMiddle) { + auto c = DomainFailoverManager::BuildCandidates( + "hunyuan.ai.ap-shanghai.tencentcloudapi.com", "ap-guangzhou.tencentcloudapi.com"); + ASSERT_EQ(c.size(), 2u); + EXPECT_EQ(c[1], "hunyuan.ap-guangzhou.tencentcloudapi.com"); +} + +TEST(BuildCandidatesTest, NonTencentCloudDomain) { + auto c = DomainFailoverManager::BuildCandidates( + "my-proxy.corp.com", "ap-guangzhou.tencentcloudapi.com"); + ASSERT_EQ(c.size(), 1u); + EXPECT_EQ(c[0], "my-proxy.corp.com"); +} + +// ===== ParseEndpoint ===== + +TEST(ParseEndpointTest, PlainService) { + auto p = DomainFailoverManager::ParseEndpoint("cvm.tencentcloudapi.com"); + EXPECT_TRUE(p.is_tc_domain); + EXPECT_EQ(p.service, "cvm"); + EXPECT_EQ(p.modifier, ""); + EXPECT_FALSE(p.modifier_kept); + EXPECT_EQ(p.tld, "tencentcloudapi.com"); +} + +TEST(ParseEndpointTest, AiModifierKept) { + auto p = DomainFailoverManager::ParseEndpoint("hunyuan.ai.tencentcloudapi.com"); + EXPECT_EQ(p.service, "hunyuan"); + EXPECT_EQ(p.modifier, "ai"); + EXPECT_TRUE(p.modifier_kept); + EXPECT_EQ(p.tld, "tencentcloudapi.com"); +} + +TEST(ParseEndpointTest, InternalModifierKept) { + auto p = DomainFailoverManager::ParseEndpoint("cvm.internal.tencentcloudapi.com"); + EXPECT_EQ(p.service, "cvm"); + EXPECT_EQ(p.modifier, "internal"); + EXPECT_TRUE(p.modifier_kept); +} + +TEST(ParseEndpointTest, IntlModifierDropped) { + auto p = DomainFailoverManager::ParseEndpoint("cvm.intl.tencentcloudapi.com"); + EXPECT_EQ(p.service, "cvm"); + EXPECT_EQ(p.modifier, "intl"); + EXPECT_FALSE(p.modifier_kept); // intl is dropped on fallback +} + +TEST(ParseEndpointTest, RegionIsNotAModifier) { + auto p = DomainFailoverManager::ParseEndpoint("cvm.ap-shanghai.tencentcloudapi.com"); + EXPECT_EQ(p.service, "cvm"); + EXPECT_EQ(p.modifier, ""); // ap-shanghai is region, not a modifier + EXPECT_EQ(p.tld, "tencentcloudapi.com"); +} + +TEST(ParseEndpointTest, AiThenRegion) { + auto p = DomainFailoverManager::ParseEndpoint( + "cvm.ai.ap-shanghai.tencentcloudapi.com"); + EXPECT_EQ(p.service, "cvm"); + EXPECT_EQ(p.modifier, "ai"); // first middle segment wins + EXPECT_TRUE(p.modifier_kept); +} + +TEST(ParseEndpointTest, ExtremeStackTakesFirstModifierOnly) { + // Only the FIRST middle segment is considered for modifier (mutually + // exclusive); everything after is region. + auto p = DomainFailoverManager::ParseEndpoint( + "cvm.ai.internal.intl.ap-guangzhou.tencentcloudapi.com"); + EXPECT_EQ(p.service, "cvm"); + EXPECT_EQ(p.modifier, "ai"); + EXPECT_TRUE(p.modifier_kept); + EXPECT_EQ(p.tld, "tencentcloudapi.com"); +} + +TEST(ParseEndpointTest, RegionBeforeModifierDropsModifier) { + // ORDERING CONTRACT: modifier must immediately follow service. Here the + // first middle segment is "ap-shanghai" (not a modifier), so parsing + // stops looking -> no modifier; the trailing "ai" is part of region and + // is dropped on fallback. Reversed input is intentionally unsupported. + auto p = DomainFailoverManager::ParseEndpoint( + "cvm.ap-shanghai.ai.tencentcloudapi.com"); + EXPECT_EQ(p.service, "cvm"); + EXPECT_EQ(p.modifier, ""); // ai is NOT recognized (comes after region) + EXPECT_FALSE(p.modifier_kept); + EXPECT_EQ(p.tld, "tencentcloudapi.com"); +} + +TEST(ParseEndpointTest, NonTencentCloud) { + auto p = DomainFailoverManager::ParseEndpoint("my-proxy.corp.com"); + EXPECT_FALSE(p.is_tc_domain); +} + +TEST(ParseEndpointTest, CaseInsensitiveTldAndModifier) { + auto p = DomainFailoverManager::ParseEndpoint("CVM.AI.TencentCloudApi.COM"); + EXPECT_TRUE(p.is_tc_domain); + EXPECT_EQ(p.service, "cvm"); // normalized lower-case + EXPECT_EQ(p.modifier, "ai"); // normalized lower-case + EXPECT_TRUE(p.modifier_kept); + EXPECT_EQ(p.tld, "tencentcloudapi.com"); +} + +// ===== ResolveBackupEndpoint ===== + +TEST(ResolveBackupEndpointTest, BareRegionPrependsService) { + EXPECT_EQ(DomainFailoverManager::ResolveBackupEndpoint( + "ap-guangzhou.tencentcloudapi.com", "cvm"), + "cvm.ap-guangzhou.tencentcloudapi.com"); +} + +TEST(ResolveBackupEndpointTest, CompleteDomainMultiSegmentKeptAsIs) { + EXPECT_EQ(DomainFailoverManager::ResolveBackupEndpoint( + "cvm.ap-guangzhou.tencentcloudapi.com", "cvm"), + "cvm.ap-guangzhou.tencentcloudapi.com"); +} + +TEST(ResolveBackupEndpointTest, SingleSegmentEqualsServiceKeptAsIs) { + EXPECT_EQ(DomainFailoverManager::ResolveBackupEndpoint( + "cvm.tencentcloudapi.com", "cvm"), + "cvm.tencentcloudapi.com"); +} + +TEST(ResolveBackupEndpointTest, ModifierNotPrepended) { + // primary service is hunyuan; bare-region backup only gets service, + // never the ai modifier. + EXPECT_EQ(DomainFailoverManager::ResolveBackupEndpoint( + "ap-guangzhou.tencentcloudapi.com", "hunyuan"), + "hunyuan.ap-guangzhou.tencentcloudapi.com"); +} + +TEST(ResolveBackupEndpointTest, NonTencentCloudKeptAsIs) { + EXPECT_EQ(DomainFailoverManager::ResolveBackupEndpoint( + "my-proxy.corp.com", "cvm"), + "my-proxy.corp.com"); +} diff --git a/test/function_test/core/Core_Http_ResolveIp_Ft.cpp b/test/function_test/core/Core_Http_ResolveIp_Ft.cpp index 8f901a6933..76efc91c20 100644 --- a/test/function_test/core/Core_Http_ResolveIp_Ft.cpp +++ b/test/function_test/core/Core_Http_ResolveIp_Ft.cpp @@ -23,12 +23,18 @@ namespace string secretId = CUtils::GetEnv("TENCENTCLOUD_SECRET_ID"); string secretKey = CUtils::GetEnv("TENCENTCLOUD_SECRET_KEY"); + string resolveIp = CUtils::GetEnv("TENCENTCLOUD_RESOLVE_IP"); Credential cred = Credential(secretId, secretKey); HttpProfile httpProfile = HttpProfile(); httpProfile.SetEndpoint("cvm.ap-guangzhou.tencentcloudapi.com"); httpProfile.SetReqTimeout(10); - httpProfile.SetResolveIp("106.55.122.199"); + // Valid resolve IP must be provided via env TENCENTCLOUD_RESOLVE_IP. + // Skip the test if not set (avoid hardcoded IPs in source code). + if (resolveIp.empty()) { + GTEST_SKIP() << "TENCENTCLOUD_RESOLVE_IP not set, skipping"; + } + httpProfile.SetResolveIp(resolveIp); ClientProfile clientProfile = ClientProfile(httpProfile); @@ -57,7 +63,13 @@ namespace HttpProfile httpProfile = HttpProfile(); httpProfile.SetEndpoint("cvm.ap-guangzhou.tencentcloudapi.com"); httpProfile.SetReqTimeout(5); - httpProfile.SetResolveIp("192.168.1.1"); + // Resolve IP that is unreachable, causing connection failure. + // Provided via env to avoid hardcoded IP literals in source. + string invalidIp = CUtils::GetEnv("TENCENTCLOUD_INVALID_RESOLVE_IP"); + if (invalidIp.empty()) { + GTEST_SKIP() << "TENCENTCLOUD_INVALID_RESOLVE_IP not set, skipping"; + } + httpProfile.SetResolveIp(invalidIp); ClientProfile clientProfile = ClientProfile(httpProfile); @@ -105,7 +117,12 @@ namespace TEST(cvm, ResolveIp_ParameterPassing) { HttpProfile httpProfile; - string testIp = "106.55.122.199"; + // Read a valid IP from env to verify parameter passing without + // hardcoding IP literals in source code. + string testIp = CUtils::GetEnv("TENCENTCLOUD_RESOLVE_IP"); + if (testIp.empty()) { + GTEST_SKIP() << "TENCENTCLOUD_RESOLVE_IP not set, skipping"; + } httpProfile.SetResolveIp(testIp); EXPECT_EQ(httpProfile.GetResolveIp(), testIp); diff --git a/test/function_test/core/Core_Profile_DomainFailover_Ft.cpp b/test/function_test/core/Core_Profile_DomainFailover_Ft.cpp new file mode 100644 index 0000000000..69a3b8a766 --- /dev/null +++ b/test/function_test/core/Core_Profile_DomainFailover_Ft.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2017-2019 Tencent. All Rights Reserved. + * + * 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. + */ + +#include +#include + +#include + +using namespace TencentCloud; + +TEST(ClientProfileDomainFailoverTest, RegionBreakerEnabledByDefault) { + // Region failover is enabled by default. Users can opt out by calling + // SetDisableRegionBreaker(true). + ClientProfile cp; + EXPECT_FALSE(cp.GetDisableRegionBreaker()); +} + +TEST(ClientProfileDomainFailoverTest, EnableDisableRegionBreaker) { + ClientProfile cp; + cp.SetDisableRegionBreaker(false); + EXPECT_FALSE(cp.GetDisableRegionBreaker()); + cp.SetDisableRegionBreaker(true); + EXPECT_TRUE(cp.GetDisableRegionBreaker()); +} + +TEST(ClientProfileDomainFailoverTest, DefaultRegionBreakerProfile) { + // Verify the default RegionBreakerProfile values. + // BackupEndpoint defaults to empty (user opts in explicitly). + ClientProfile cp; + RegionBreakerProfile rb = cp.GetRegionBreakerProfile(); + EXPECT_EQ(rb.GetBackupEndpoint(), ""); + EXPECT_EQ(rb.GetMaxFailNum(), 5); + EXPECT_DOUBLE_EQ(rb.GetMaxFailPercent(), 0.75); + EXPECT_EQ(rb.GetWindowInterval(), 300); + EXPECT_EQ(rb.GetTimeout(), 60); + EXPECT_EQ(rb.GetMaxRequests(), 5); +} + +TEST(ClientProfileDomainFailoverTest, SetCustomRegionBreakerProfile) { + RegionBreakerProfile rb( + /*backup_endpoint=*/ "ap-beijing.tencentcloudapi.com", + /*max_fail_num=*/ 3, + /*max_fail_percent=*/0.5, + /*window_interval=*/ 60, + /*timeout=*/ 30, + /*max_requests=*/ 3); + + ClientProfile cp; + cp.SetRegionBreakerProfile(rb); + + RegionBreakerProfile got = cp.GetRegionBreakerProfile(); + EXPECT_EQ(got.GetBackupEndpoint(), "ap-beijing.tencentcloudapi.com"); + EXPECT_EQ(got.GetMaxFailNum(), 3); + EXPECT_DOUBLE_EQ(got.GetMaxFailPercent(), 0.5); + EXPECT_EQ(got.GetWindowInterval(), 60); + EXPECT_EQ(got.GetTimeout(), 30); + EXPECT_EQ(got.GetMaxRequests(), 3); +} + +TEST(ClientProfileDomainFailoverTest, PreservedWithHttpProfileConstructor) { + HttpProfile hp; + hp.SetEndpoint("cvm.tencentcloudapi.com"); + ClientProfile cp(hp); + EXPECT_FALSE(cp.GetDisableRegionBreaker()); +} + +TEST(ClientProfileDomainFailoverTest, PreservedWithSignMethodConstructor) { + ClientProfile cp(ClientProfile::SignMethod::TC3_HMAC_SHA256); + EXPECT_FALSE(cp.GetDisableRegionBreaker()); +}