1717
1818package org .apache .doris .plugin .dialect ;
1919
20- import com .github .benmanes .caffeine .cache .Cache ;
21- import com .github .benmanes .caffeine .cache .Caffeine ;
22- import com .google .common .collect .Lists ;
2320import com .google .gson .Gson ;
2421import com .google .gson .reflect .TypeToken ;
2522import lombok .Data ;
3330import java .net .HttpURLConnection ;
3431import java .net .URL ;
3532import java .nio .charset .StandardCharsets ;
36- import java .util .Collections ;
37- import java .util .Date ;
38- import java .util .List ;
39- import java .util .concurrent .ConcurrentHashMap ;
40- import java .util .concurrent .ThreadLocalRandom ;
41- import java .util .concurrent .TimeUnit ;
4233
4334/**
4435 * This class is used to convert sql with different dialects using sql convertor service.
4536 * The sql convertor service is a http service which is used to convert sql.
46- * <p>
47- * Features:
48- * - Support multiple URLs (comma separated)
49- * - Blacklist mechanism for failed URLs
50- * - Automatic failover and retry
51- * - URL caching and smart selection
5237 */
5338public class HttpDialectUtils {
5439 private static final Logger LOG = LogManager .getLogger (HttpDialectUtils .class );
5540
56- // Cache URL manager instances to avoid duplicate parsing with automatic expiration
57- private static final Cache <String , UrlManager > urlManagerCache = Caffeine .newBuilder ()
58- .maximumSize (10 )
59- .expireAfterAccess (8 , TimeUnit .HOURS )
60- .build ();
61-
62- // Blacklist recovery time (ms): 1 minute
63- private static final long BLACKLIST_RECOVERY_TIME_MS = 60 * 1000 ;
64- // Connection timeout period (ms): 3 seconds
65- private static final int CONNECTION_TIMEOUT_MS = 3000 ;
66- // Read timeout period (ms): 10 seconds
67- private static final int READ_TIMEOUT_MS = 10000 ;
68-
69- public static String convertSql (String targetURLs , String originStmt , String dialect ,
41+ public static String convertSql (String targetURL , String originStmt , String dialect ,
7042 String [] features , String config ) {
71- UrlManager urlManager = getOrCreateUrlManager (targetURLs );
7243 ConvertRequest convertRequest = new ConvertRequest (originStmt , dialect , features , config );
73- String requestStr = convertRequest .toJson ();
74-
75- // Try to convert SQL using intelligent URL selection strategy
76- return tryConvertWithIntelligentSelection (urlManager , requestStr , originStmt );
77- }
78-
79- /**
80- * Try to convert SQL using intelligent URL selection strategy
81- * CRITICAL: This method ensures 100% success rate when ANY service is available
82- */
83- private static String tryConvertWithIntelligentSelection (
84- UrlManager urlManager , String requestStr , String originStmt ) {
85- // Strategy: Try ALL URLs in intelligent order, regardless of blacklist status
86- // This ensures 100% success rate when any service is actually available
8744
88- List <String > allUrls = urlManager .getAllUrlsInPriorityOrder ();
89-
90- for (String url : allUrls ) {
91- try {
92- String result = doConvertSql (url , requestStr );
93- // If no exception thrown, HTTP response was successful (200)
94- // Mark URL as healthy and return result (even if empty)
95- urlManager .markUrlAsHealthy (url );
96- if (LOG .isDebugEnabled ()) {
97- LOG .debug ("Successfully converted SQL using URL: {}" , url );
98- }
99- return result ;
100- } catch (Exception e ) {
101- LOG .warn ("Failed to convert SQL using URL: {}, error: {}" , url , e .getMessage ());
102- // Add failed URL to blacklist for future optimization
103- urlManager .markUrlAsBlacklisted (url );
104- // Continue trying next URL - this is CRITICAL for 100% success rate
105- }
106- }
107-
108- return originStmt ;
109- }
110-
111- /**
112- * Get or create a URL manager
113- */
114- private static UrlManager getOrCreateUrlManager (String targetURLs ) {
115- return urlManagerCache .get (targetURLs , UrlManager ::new );
116- }
117-
118- /**
119- * Perform SQL conversion for individual URL
120- */
121- private static String doConvertSql (String targetURL , String requestStr ) throws Exception {
12245 HttpURLConnection connection = null ;
12346 try {
124- if (targetURL == null || targetURL .trim ().isEmpty ()) {
125- throw new Exception ("Target URL is null or empty" );
126- }
127- URL url = new URL (targetURL .trim ());
47+ URL url = new URL (targetURL );
12848 connection = (HttpURLConnection ) url .openConnection ();
12949 connection .setRequestMethod ("POST" );
13050 connection .setRequestProperty ("Content-Type" , "application/json" );
13151 connection .setUseCaches (false );
13252 connection .setDoOutput (true );
133- connection .setConnectTimeout (CONNECTION_TIMEOUT_MS );
134- connection .setReadTimeout (READ_TIMEOUT_MS );
13553
54+ String requestStr = convertRequest .toJson ();
13655 try (OutputStream outputStream = connection .getOutputStream ()) {
13756 outputStream .write (requestStr .getBytes (StandardCharsets .UTF_8 ));
13857 }
13958
14059 int responseCode = connection .getResponseCode ();
14160 if (LOG .isDebugEnabled ()) {
142- LOG .debug ("POST Response Code: {}, URL: {}, post data: {}" , responseCode , targetURL , requestStr );
61+ LOG .debug ("POST Response Code: {}, post data: {}" , responseCode , requestStr );
14362 }
14463
14564 if (responseCode == HttpURLConnection .HTTP_OK ) {
@@ -157,192 +76,33 @@ private static String doConvertSql(String targetURL, String requestStr) throws E
15776 }.getType ();
15877 ConvertResponse result = new Gson ().fromJson (response .toString (), type );
15978 if (LOG .isDebugEnabled ()) {
160- LOG .debug ("Convert response: {}, URL: {} " , result , targetURL );
79+ LOG .debug ("convert response: {}" , result );
16180 }
16281 if (result .code == 0 ) {
16382 if (!"v1" .equals (result .version )) {
164- throw new Exception ("Unsupported version: " + result .version );
83+ LOG .warn ("failed to convert sql, response version is not v1: {}" , result .version );
84+ return originStmt ;
16585 }
16686 return result .data ;
16787 } else {
168- throw new Exception ("Conversion failed: " + result .message );
88+ LOG .warn ("failed to convert sql, response: {}" , result );
89+ return originStmt ;
16990 }
17091 }
17192 } else {
172- throw new Exception ("HTTP response code: " + responseCode );
93+ LOG .warn ("failed to convert sql, response code: {}" , responseCode );
94+ return originStmt ;
17395 }
96+ } catch (Exception e ) {
97+ LOG .warn ("failed to convert sql" , e );
98+ return originStmt ;
17499 } finally {
175100 if (connection != null ) {
176101 connection .disconnect ();
177102 }
178103 }
179104 }
180105
181- /**
182- * URL Manager - Responsible for URL parsing, caching, blacklist management, and smart selection
183- */
184- private static class UrlManager {
185- private final List <String > parsedUrls ;
186- private final ConcurrentHashMap <String , BlacklistEntry > blacklist ;
187-
188- public UrlManager (String urls ) {
189- this .parsedUrls = parseUrls (urls );
190- this .blacklist = new ConcurrentHashMap <>();
191- if (LOG .isDebugEnabled ()) {
192- LOG .debug ("Created UrlManager with URLs: {}, parsed: {}" , urls , parsedUrls );
193- }
194- }
195-
196- /**
197- * Parse comma separated URL strings
198- */
199- private List <String > parseUrls (String urls ) {
200- List <String > result = Lists .newArrayList ();
201- if (urls != null && !urls .trim ().isEmpty ()) {
202- String [] urlArray = urls .split ("," );
203- for (String url : urlArray ) {
204- String trimmedUrl = url .trim ();
205- if (!trimmedUrl .isEmpty ()) {
206- result .add (trimmedUrl );
207- }
208- }
209- }
210- return result ;
211- }
212-
213- /**
214- * Mark URL as healthy (remove from blacklist)
215- */
216- public void markUrlAsHealthy (String url ) {
217- if (blacklist .remove (url ) != null ) {
218- LOG .info ("Removed URL from blacklist due to successful request: {}" , url );
219- }
220- }
221-
222- /**
223- * Add URL to blacklist
224- */
225- public void markUrlAsBlacklisted (String url ) {
226- // If URL is already in blacklist, just return
227- if (blacklist .containsKey (url )) {
228- return ;
229- }
230-
231- long currentTime = System .currentTimeMillis ();
232- long recoverTime = currentTime + BLACKLIST_RECOVERY_TIME_MS ;
233- blacklist .put (url , new BlacklistEntry (currentTime , recoverTime ));
234- LOG .warn ("Added URL to blacklist: {}, will recover at: {}" , url , new Date (recoverTime ));
235- }
236-
237- /**
238- * Check if URL is localhost (127.0.0.1 or localhost)
239- */
240- private boolean isLocalhost (String url ) {
241- return url .contains ("127.0.0.1" ) || url .contains ("localhost" );
242- }
243-
244- /**
245- * Get ALL URLs in priority order for 100% success guarantee
246- * CRITICAL: This method ensures we try every URL when any service might be available
247- * <p>
248- * Priority order:
249- * 1. Localhost URLs (127.0.0.1 or localhost) that are healthy
250- * 2. Other healthy URLs (randomly selected)
251- * 3. Localhost URLs in blacklist
252- * 4. Other blacklisted URLs (sorted by recovery time)
253- */
254- public List <String > getAllUrlsInPriorityOrder () {
255- List <String > prioritizedUrls = Lists .newArrayList ();
256- List <String > healthyLocalhost = Lists .newArrayList ();
257- List <String > healthyOthers = Lists .newArrayList ();
258- List <String > blacklistedLocalhost = Lists .newArrayList ();
259- List <String > blacklistedOthers = Lists .newArrayList ();
260-
261- long currentTime = System .currentTimeMillis ();
262-
263- // Single traversal to categorize all URLs
264- for (String url : parsedUrls ) {
265- BlacklistEntry entry = blacklist .get (url );
266- boolean isHealthy = false ;
267-
268- if (entry == null ) {
269- // URL is not in blacklist, consider it healthy
270- isHealthy = true ;
271- } else if (currentTime >= entry .recoverTime ) {
272- // URL has reached recovery time, remove from blacklist and consider healthy
273- blacklist .remove (url );
274- isHealthy = true ;
275- if (LOG .isDebugEnabled ()) {
276- LOG .debug ("URL recovered from blacklist: {}" , url );
277- }
278- }
279-
280- boolean isLocal = isLocalhost (url );
281-
282- if (isHealthy ) {
283- if (isLocal ) {
284- healthyLocalhost .add (url );
285- } else {
286- healthyOthers .add (url );
287- }
288- } else {
289- if (isLocal ) {
290- blacklistedLocalhost .add (url );
291- } else {
292- blacklistedOthers .add (url );
293- }
294- }
295- }
296-
297- // Add URLs in priority order
298- // 1. Healthy localhost URLs first
299- prioritizedUrls .addAll (healthyLocalhost );
300-
301- // 2. Other healthy URLs (randomly shuffled for load balancing)
302- Collections .shuffle (healthyOthers , ThreadLocalRandom .current ());
303- prioritizedUrls .addAll (healthyOthers );
304-
305- // 3. Blacklisted localhost URLs
306- prioritizedUrls .addAll (blacklistedLocalhost );
307-
308- // 4. Other blacklisted URLs (sorted by recovery time)
309- blacklistedOthers .sort ((url1 , url2 ) -> {
310- BlacklistEntry entry1 = blacklist .get (url1 );
311- BlacklistEntry entry2 = blacklist .get (url2 );
312- if (entry1 == null && entry2 == null ) {
313- return 0 ;
314- }
315- if (entry1 == null ) {
316- return -1 ;
317- }
318- if (entry2 == null ) {
319- return 1 ;
320- }
321- return Long .compare (entry1 .recoverTime , entry2 .recoverTime );
322- });
323- prioritizedUrls .addAll (blacklistedOthers );
324-
325- if (LOG .isDebugEnabled ()) {
326- LOG .debug ("All URLs in priority order: {}" , prioritizedUrls );
327- }
328-
329- return prioritizedUrls ;
330- }
331- }
332-
333- /**
334- * Blacklist entry
335- */
336- private static class BlacklistEntry {
337- final long blacklistedTime ;
338- final long recoverTime ;
339-
340- BlacklistEntry (long blacklistedTime , long recoverTime ) {
341- this .blacklistedTime = blacklistedTime ;
342- this .recoverTime = recoverTime ;
343- }
344- }
345-
346106 @ Data
347107 private static class ConvertRequest {
348108 private String version ; // CHECKSTYLE IGNORE THIS LINE
0 commit comments