diff --git a/src/main/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientImpl.java b/src/main/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientImpl.java index 85f7abcad..390db7f51 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientImpl.java +++ b/src/main/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientImpl.java @@ -4,9 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -65,18 +65,6 @@ public LeetcodeClientImpl(final MeterRegistry meterRegistry, final LeetcodeAuthS this.leetcodeAuthStealer = leetcodeAuthStealer; } - private Timer timer() { - var stackFrame = StackWalker.getInstance() - // skip timer() invocation - .walk(frames -> frames.skip(1).findFirst()) - .orElseThrow(); - - String methodName = stackFrame.getMethodName(); - String className = stackFrame.getClassName(); - - return meterRegistry.timer(TIMED_METRIC_NAME, "class", className, "method", methodName); - } - private Counter errorCounter() { var stackFrame = StackWalker.getInstance() .walk(frames -> frames.skip(1) @@ -139,77 +127,78 @@ private HttpRequest.Builder getGraphQLRequestBuilder() { } @Override + @Timed(value = TIMED_METRIC_NAME) public LeetcodeQuestion findQuestionBySlug(final String slug) { - return timer().record(() -> { - String requestBody; - try { - requestBody = SelectProblemQuery.body(slug); - } catch (Exception e) { - throw new RuntimeException("Error building the request body"); - } - - try { - HttpRequest request = getGraphQLRequestBuilder() - .POST(BodyPublishers.ofString(requestBody)) - .build(); - - HttpResponse response = client.send(request, BodyHandlers.ofString()); - int statusCode = response.statusCode(); - String body = response.body(); - - if (statusCode != 200) { - if (isThrottled(statusCode)) { - leetcodeAuthStealer.reloadCookie(); - } - throw new RuntimeException("API Returned status " + statusCode + ": " + body); - } - - JsonNode node = mapper.readTree(body); - - int questionId = - node.path("data").path("question").path("questionId").asInt(); - String questionTitle = - node.path("data").path("question").path("title").asText(); - String titleSlug = - node.path("data").path("question").path("titleSlug").asText(); - String link = "https://leetcode.com/problems/" + titleSlug; - String difficulty = - node.path("data").path("question").path("difficulty").asText(); - String question = - node.path("data").path("question").path("content").asText(); - - String statsJson = - node.path("data").path("question").path("stats").asText(); - JsonNode stats = mapper.readTree(statsJson); - String acRateString = stats.get("acRate").asText(); - float acRate = Float.parseFloat(acRateString.replace("%", "")) / 100f; + String requestBody; + try { + requestBody = SelectProblemQuery.body(slug); + } catch (Exception e) { + throw new IllegalArgumentException("Error building the request body", e); + } - JsonNode topicTagsNode = node.path("data").path("question").path("topicTags"); + try { + HttpRequest request = getGraphQLRequestBuilder() + .POST(BodyPublishers.ofString(requestBody)) + .build(); - List tags = new ArrayList<>(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int statusCode = response.statusCode(); + String body = response.body(); - for (JsonNode el : topicTagsNode) { - tags.add(LeetcodeTopicTag.builder() - .name(el.get("name").asText()) - .slug(el.get("slug").asText()) - .build()); + if (statusCode != 200) { + if (isThrottled(statusCode)) { + leetcodeAuthStealer.reloadCookie(); } + throw new IllegalArgumentException("API Returned status " + statusCode + ": " + body); + } - return LeetcodeQuestion.builder() - .link(link) - .questionId(questionId) - .questionTitle(questionTitle) - .titleSlug(titleSlug) - .difficulty(difficulty) - .question(question) - .acceptanceRate(acRate) - .topics(tags) - .build(); - } catch (Exception e) { - errorCounter().increment(); - throw new RuntimeException("Error fetching the API", e); + JsonNode node = mapper.readTree(body); + + int questionId = + node.path("data").path("question").path("questionId").asInt(); + String questionTitle = + node.path("data").path("question").path("title").asText(); + String titleSlug = + node.path("data").path("question").path("titleSlug").asText(); + String link = "https://leetcode.com/problems/" + titleSlug; + String difficulty = + node.path("data").path("question").path("difficulty").asText(); + String question = node.path("data").path("question").path("content").asText(); + + String statsJson = node.path("data").path("question").path("stats").asText(); + JsonNode stats = mapper.readTree(statsJson); + String acRateString = stats.get("acRate").asText(); + float acRate = Float.parseFloat(acRateString.replace("%", "")) / 100f; + + JsonNode topicTagsNode = node.path("data").path("question").path("topicTags"); + + List tags = new ArrayList<>(); + + for (JsonNode el : topicTagsNode) { + tags.add(LeetcodeTopicTag.builder() + .name(el.get("name").asText()) + .slug(el.get("slug").asText()) + .build()); } - }); + + return LeetcodeQuestion.builder() + .link(link) + .questionId(questionId) + .questionTitle(questionTitle) + .titleSlug(titleSlug) + .difficulty(difficulty) + .question(question) + .acceptanceRate(acRate) + .topics(tags) + .build(); + } catch (InterruptedException e) { + errorCounter().increment(); + Thread.currentThread().interrupt(); + throw new IllegalArgumentException("Thread interrupted", e); + } catch (Exception e) { + errorCounter().increment(); + throw new IllegalArgumentException("Error fetching the API", e); + } } @Override @@ -218,316 +207,324 @@ public ArrayList findSubmissionsByUsername(final String user } @Override + @Timed(value = TIMED_METRIC_NAME) public ArrayList findSubmissionsByUsername(final String username, final int limit) { - return timer().record(() -> { - ArrayList submissions = new ArrayList<>(); - - String requestBody; - try { - requestBody = SelectAcceptedSubmisisonsQuery.body(username, limit); - } catch (Exception e) { - throw new RuntimeException("Error building the request body"); - } + ArrayList submissions = new ArrayList<>(); - try { - HttpRequest request = getGraphQLRequestBuilder() - .POST(BodyPublishers.ofString(requestBody)) - .build(); + String requestBody; + try { + requestBody = SelectAcceptedSubmisisonsQuery.body(username, limit); + } catch (Exception e) { + throw new IllegalArgumentException("Error building the request body"); + } - HttpResponse response = client.send(request, BodyHandlers.ofString()); - int statusCode = response.statusCode(); - String body = response.body(); + try { + HttpRequest request = getGraphQLRequestBuilder() + .POST(BodyPublishers.ofString(requestBody)) + .build(); - if (statusCode != 200) { - if (isThrottled(statusCode)) { - leetcodeAuthStealer.reloadCookie(); - } - throw new RuntimeException("API Returned status " + statusCode + ": " + body); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int statusCode = response.statusCode(); + String body = response.body(); + + if (statusCode != 200) { + if (isThrottled(statusCode)) { + leetcodeAuthStealer.reloadCookie(); } + throw new IllegalArgumentException("API Returned status " + statusCode + ": " + body); + } + + JsonNode node = mapper.readTree(body); + JsonNode submissionsNode = node.path("data").path("recentAcSubmissionList"); - JsonNode node = mapper.readTree(body); - JsonNode submissionsNode = node.path("data").path("recentAcSubmissionList"); - - if (submissionsNode.isArray()) { - if (submissionsNode.isEmpty() || submissionsNode == null) { - return submissions; - } - - for (JsonNode submission : submissionsNode) { - int id = submission.path("id").asInt(); - String title = submission.path("title").asText(); - String titleSlug = submission.path("titleSlug").asText(); - String timestampString = submission.path("timestamp").asText(); - long epochSeconds = Long.parseLong(timestampString); - Instant instant = Instant.ofEpochSecond(epochSeconds); - - LocalDateTime timestamp = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); - String statusDisplay = submission.path("statusDisplay").asText(); - submissions.add(new LeetcodeSubmission(id, title, titleSlug, timestamp, statusDisplay)); - } + if (submissionsNode.isArray()) { + if (submissionsNode.isEmpty() || submissionsNode == null) { + return submissions; } - return submissions; - } catch (Exception e) { - errorCounter().increment(); - throw new RuntimeException("Error fetching the API", e); + for (JsonNode submission : submissionsNode) { + int id = submission.path("id").asInt(); + String title = submission.path("title").asText(); + String titleSlug = submission.path("titleSlug").asText(); + String timestampString = submission.path("timestamp").asText(); + long epochSeconds = Long.parseLong(timestampString); + Instant instant = Instant.ofEpochSecond(epochSeconds); + + LocalDateTime timestamp = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + String statusDisplay = submission.path("statusDisplay").asText(); + submissions.add(new LeetcodeSubmission(id, title, titleSlug, timestamp, statusDisplay)); + } } - }); + + return submissions; + } catch (InterruptedException e) { + errorCounter().increment(); + Thread.currentThread().interrupt(); + throw new IllegalArgumentException("Thread interrupted", e); + } catch (Exception e) { + errorCounter().increment(); + throw new IllegalArgumentException("Error fetching the API", e); + } } @Override + @Timed(value = TIMED_METRIC_NAME) public LeetcodeDetailedQuestion findSubmissionDetailBySubmissionId(final int submissionId) { - return timer().record(() -> { - String requestBody; - try { - requestBody = GetSubmissionDetails.body(submissionId); - } catch (Exception e) { - throw new RuntimeException("Error building the request body"); - } - - try { - HttpRequest request = getGraphQLRequestBuilder() - .POST(BodyPublishers.ofString(requestBody)) - .build(); + String requestBody; + try { + requestBody = GetSubmissionDetails.body(submissionId); + } catch (Exception e) { + throw new IllegalArgumentException("Error building the request body"); + } - HttpResponse response = client.send(request, BodyHandlers.ofString()); - int statusCode = response.statusCode(); - String body = response.body(); + try { + HttpRequest request = getGraphQLRequestBuilder() + .POST(BodyPublishers.ofString(requestBody)) + .build(); - if (statusCode != 200) { - if (isThrottled(statusCode)) { - leetcodeAuthStealer.reloadCookie(); - } - throw new RuntimeException("API Returned status " + statusCode + ": " + body); - } + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int statusCode = response.statusCode(); + String body = response.body(); - JsonNode node = mapper.readTree(body); - JsonNode baseNode = node.path("data").path("submissionDetails"); - - int runtime = baseNode.path("runtime").asInt(); - String runtimeDisplay = baseNode.path("runtimeDisplay").asText(); - float runtimePercentile = - (float) baseNode.path("runtimePercentile").asDouble(); - int memory = baseNode.path("memory").asInt(); - String memoryDisplay = baseNode.path("memoryDisplay").asText(); - float memoryPercentile = - (float) baseNode.path("memoryPercentile").asDouble(); - String code = baseNode.path("code").asText(); - String langName = baseNode.path("lang").path("name").asText(); - String langVerboseName = - baseNode.path("lang").path("verboseName").asText(); - Lang lang = (Strings.isNullOrEmpty(langName) || Strings.isNullOrEmpty(langVerboseName)) - ? null - : new Lang(langName, langVerboseName); - - // if any of these are empty, then extremely likely that we're throttled. - if (Strings.isNullOrEmpty(runtimeDisplay) || Strings.isNullOrEmpty(memoryDisplay)) { + if (statusCode != 200) { + if (isThrottled(statusCode)) { leetcodeAuthStealer.reloadCookie(); } + throw new IllegalArgumentException("API Returned status " + statusCode + ": " + body); + } - LeetcodeDetailedQuestion question = new LeetcodeDetailedQuestion( - runtime, - runtimeDisplay, - runtimePercentile, - memory, - memoryDisplay, - memoryPercentile, - code, - lang); - - return question; - } catch (Exception e) { - errorCounter().increment(); - throw new RuntimeException("Error fetching the API", e); + JsonNode node = mapper.readTree(body); + JsonNode baseNode = node.path("data").path("submissionDetails"); + + int runtime = baseNode.path("runtime").asInt(); + String runtimeDisplay = baseNode.path("runtimeDisplay").asText(); + float runtimePercentile = (float) baseNode.path("runtimePercentile").asDouble(); + int memory = baseNode.path("memory").asInt(); + String memoryDisplay = baseNode.path("memoryDisplay").asText(); + float memoryPercentile = (float) baseNode.path("memoryPercentile").asDouble(); + String code = baseNode.path("code").asText(); + String langName = baseNode.path("lang").path("name").asText(); + String langVerboseName = baseNode.path("lang").path("verboseName").asText(); + Lang lang = (Strings.isNullOrEmpty(langName) || Strings.isNullOrEmpty(langVerboseName)) + ? null + : new Lang(langName, langVerboseName); + + // if any of these are empty, then extremely likely that we're throttled. + if (Strings.isNullOrEmpty(runtimeDisplay) || Strings.isNullOrEmpty(memoryDisplay)) { + leetcodeAuthStealer.reloadCookie(); } - }); + + LeetcodeDetailedQuestion question = new LeetcodeDetailedQuestion( + runtime, runtimeDisplay, runtimePercentile, memory, memoryDisplay, memoryPercentile, code, lang); + + return question; + } catch (InterruptedException e) { + errorCounter().increment(); + Thread.currentThread().interrupt(); + throw new IllegalArgumentException("Thread interrupted", e); + } catch (Exception e) { + errorCounter().increment(); + throw new IllegalArgumentException("Error fetching the API", e); + } } + @Timed(value = TIMED_METRIC_NAME) public POTD getPotd() { - return timer().record(() -> { - String requestBody; - try { - requestBody = GetPotd.body(); - } catch (Exception e) { - throw new RuntimeException("Error building the request body"); - } + String requestBody; + try { + requestBody = GetPotd.body(); + } catch (Exception e) { + throw new IllegalArgumentException("Error building the request body"); + } - try { - HttpRequest request = getGraphQLRequestBuilder() - .POST(BodyPublishers.ofString(requestBody)) - .build(); + try { + HttpRequest request = getGraphQLRequestBuilder() + .POST(BodyPublishers.ofString(requestBody)) + .build(); - HttpResponse response = client.send(request, BodyHandlers.ofString()); - int statusCode = response.statusCode(); - String body = response.body(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int statusCode = response.statusCode(); + String body = response.body(); - if (statusCode != 200) { - if (isThrottled(statusCode)) { - leetcodeAuthStealer.reloadCookie(); - } - throw new RuntimeException("API Returned status " + statusCode + ": " + body); + if (statusCode != 200) { + if (isThrottled(statusCode)) { + leetcodeAuthStealer.reloadCookie(); } - - JsonNode node = mapper.readTree(body); - JsonNode baseNode = node.path("data") - .path("activeDailyCodingChallengeQuestion") - .path("question"); - - String titleSlug = baseNode.path("titleSlug").asText(); - String title = baseNode.path("title").asText(); - var difficulty = - QuestionDifficulty.valueOf(baseNode.path("difficulty").asText()); - - return new POTD(title, titleSlug, difficulty); - } catch (Exception e) { - errorCounter().increment(); - throw new RuntimeException("Error fetching the API", e); + throw new IllegalArgumentException("API Returned status " + statusCode + ": " + body); } - }); + + JsonNode node = mapper.readTree(body); + JsonNode baseNode = + node.path("data").path("activeDailyCodingChallengeQuestion").path("question"); + + String titleSlug = baseNode.path("titleSlug").asText(); + String title = baseNode.path("title").asText(); + var difficulty = + QuestionDifficulty.valueOf(baseNode.path("difficulty").asText()); + + return new POTD(title, titleSlug, difficulty); + } catch (InterruptedException e) { + errorCounter().increment(); + Thread.currentThread().interrupt(); + throw new IllegalArgumentException("Thread interrupted", e); + } catch (Exception e) { + errorCounter().increment(); + throw new IllegalArgumentException("Error fetching the API", e); + } } @Override + @Timed(value = TIMED_METRIC_NAME) public UserProfile getUserProfile(final String username) { - return timer().record(() -> { - String requestBody; - try { - requestBody = GetUserProfile.body(username); - } catch (Exception e) { - throw new RuntimeException("Error building the request body", e); - } + String requestBody; + try { + requestBody = GetUserProfile.body(username); + } catch (Exception e) { + throw new IllegalArgumentException("Error building the request body", e); + } - try { - HttpRequest request = getGraphQLRequestBuilder() - .POST(BodyPublishers.ofString(requestBody)) - .build(); + try { + HttpRequest request = getGraphQLRequestBuilder() + .POST(BodyPublishers.ofString(requestBody)) + .build(); - HttpResponse response = client.send(request, BodyHandlers.ofString()); - int statusCode = response.statusCode(); - String body = response.body(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int statusCode = response.statusCode(); + String body = response.body(); - if (statusCode != 200) { - if (isThrottled(statusCode)) { - leetcodeAuthStealer.reloadCookie(); - } - throw new RuntimeException("API Returned status " + statusCode + ": " + body); + if (statusCode != 200) { + if (isThrottled(statusCode)) { + leetcodeAuthStealer.reloadCookie(); } - - JsonNode node = mapper.readTree(body); - JsonNode baseNode = node.path("data").path("matchedUser"); - - var returnedUsername = baseNode.path("username").asText(); - var ranking = baseNode.path("profile").path("ranking").asText(); - var userAvatar = baseNode.path("profile").path("userAvatar").asText(); - var realName = baseNode.path("profile").path("realName").asText(); - var aboutMe = baseNode.path("profile").path("aboutMe").asText().trim(); - - return new UserProfile(returnedUsername, ranking, userAvatar, realName, aboutMe); - } catch (Exception e) { - errorCounter().increment(); - throw new RuntimeException("Error fetching the API", e); + throw new IllegalArgumentException("API Returned status " + statusCode + ": " + body); } - }); + + JsonNode node = mapper.readTree(body); + JsonNode baseNode = node.path("data").path("matchedUser"); + + var returnedUsername = baseNode.path("username").asText(); + var ranking = baseNode.path("profile").path("ranking").asText(); + var userAvatar = baseNode.path("profile").path("userAvatar").asText(); + var realName = baseNode.path("profile").path("realName").asText(); + var aboutMe = baseNode.path("profile").path("aboutMe").asText().trim(); + + return new UserProfile(returnedUsername, ranking, userAvatar, realName, aboutMe); + } catch (InterruptedException e) { + errorCounter().increment(); + Thread.currentThread().interrupt(); + throw new IllegalArgumentException("Thread interrupted", e); + } catch (Exception e) { + errorCounter().increment(); + throw new IllegalArgumentException("Error fetching the API", e); + } } @Override + @Timed(value = TIMED_METRIC_NAME) public Set getAllTopicTags() { - return timer().record(() -> { - try { - HttpRequest request = getGraphQLRequestBuilder() - .POST(BodyPublishers.ofString(GetTopics.body())) - .build(); - - HttpResponse response = client.send(request, BodyHandlers.ofString()); - int statusCode = response.statusCode(); - String body = response.body(); - - if (statusCode != 200) { - if (isThrottled(statusCode)) { - leetcodeAuthStealer.reloadCookie(); - } - throw new RuntimeException( - "Non-successful response getting topics from Leetcode API. Status code: " + statusCode); - } + try { + HttpRequest request = getGraphQLRequestBuilder() + .POST(BodyPublishers.ofString(GetTopics.body())) + .build(); - JsonNode json = mapper.readTree(body); - JsonNode edges = json.path("data").path("questionTopicTags").path("edges"); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int statusCode = response.statusCode(); + String body = response.body(); - if (!edges.isArray()) { - throw new RuntimeException("The expected shape of getting topics did not match the received body"); + if (statusCode != 200) { + if (isThrottled(statusCode)) { + leetcodeAuthStealer.reloadCookie(); } + throw new IllegalArgumentException( + "Non-successful response getting topics from Leetcode API. Status code: " + statusCode); + } - Set result = new HashSet<>(); + JsonNode json = mapper.readTree(body); + JsonNode edges = json.path("data").path("questionTopicTags").path("edges"); - for (JsonNode edge : edges) { - JsonNode node = edge.path("node"); - result.add(LeetcodeTopicTag.builder() - .name(node.get("name").asText()) - .slug(node.get("slug").asText()) - .build()); - } + if (!edges.isArray()) { + throw new IllegalArgumentException( + "The expected shape of getting topics did not match the received body"); + } + + Set result = new HashSet<>(); - return result; - } catch (Exception e) { - errorCounter().increment(); - throw new RuntimeException("Error getting topics from Leetcode API", e); + for (JsonNode edge : edges) { + JsonNode node = edge.path("node"); + result.add(LeetcodeTopicTag.builder() + .name(node.get("name").asText()) + .slug(node.get("slug").asText()) + .build()); } - }); + + return result; + } catch (InterruptedException e) { + errorCounter().increment(); + Thread.currentThread().interrupt(); + throw new IllegalArgumentException("Thread interrupted", e); + } catch (Exception e) { + errorCounter().increment(); + throw new IllegalArgumentException("Error getting topics from Leetcode API", e); + } } + @Timed(value = TIMED_METRIC_NAME) public List getAllProblems() { - return timer().record(() -> { - try { - HttpRequest request = getGraphQLRequestBuilder() - .POST(BodyPublishers.ofString(GetAllProblems.body())) - .build(); - HttpResponse response = client.send(request, BodyHandlers.ofString()); - int statusCode = response.statusCode(); - String body = response.body(); - if (statusCode != 200) { - if (isThrottled(statusCode)) { - leetcodeAuthStealer.reloadCookie(); - } - throw new RuntimeException( - "Non-successful response getting all questions from Leetcode API. Status code: " - + statusCode); + try { + HttpRequest request = getGraphQLRequestBuilder() + .POST(BodyPublishers.ofString(GetAllProblems.body())) + .build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int statusCode = response.statusCode(); + String body = response.body(); + if (statusCode != 200) { + if (isThrottled(statusCode)) { + leetcodeAuthStealer.reloadCookie(); } + throw new IllegalArgumentException( + "Non-successful response getting all questions from Leetcode API. Status code: " + statusCode); + } - JsonNode json = mapper.readTree(body); - JsonNode allQuestions = - json.path("data").path("problemsetQuestionListV2").path("questions"); + JsonNode json = mapper.readTree(body); + JsonNode allQuestions = + json.path("data").path("problemsetQuestionListV2").path("questions"); - if (!allQuestions.isArray()) { - throw new RuntimeException("The expected shape of getting topics did not match the received body"); - } + if (!allQuestions.isArray()) { + throw new IllegalArgumentException( + "The expected shape of getting topics did not match the received body"); + } - List result = new ArrayList<>(); - for (JsonNode question : allQuestions) { - JsonNode topicTags = question.get("topicTags"); - - List tags = new ArrayList<>(); - for (JsonNode tag : topicTags) { - tags.add(LeetcodeTopicTag.builder() - .name(tag.get("name").asText()) - .slug(tag.get("slug").asText()) - .build()); - } - - result.add(LeetcodeQuestion.builder() - .link("https://leetcode.com/problems/" - + question.get("titleSlug").asText()) - .questionId(question.get("questionFrontendId").asInt()) - .questionTitle(question.get("title").asText()) - .titleSlug(question.get("titleSlug").asText()) - .difficulty(question.get("difficulty").asText()) - .acceptanceRate((float) question.get("acRate").asDouble()) - .topics(tags) + List result = new ArrayList<>(); + for (JsonNode question : allQuestions) { + JsonNode topicTags = question.get("topicTags"); + + List tags = new ArrayList<>(); + for (JsonNode tag : topicTags) { + tags.add(LeetcodeTopicTag.builder() + .name(tag.get("name").asText()) + .slug(tag.get("slug").asText()) .build()); } - return result; - } catch (Exception e) { - errorCounter().increment(); - throw new RuntimeException("Error getting all problems from Leetcode API", e); + + result.add(LeetcodeQuestion.builder() + .link("https://leetcode.com/problems/" + + question.get("titleSlug").asText()) + .questionId(question.get("questionFrontendId").asInt()) + .questionTitle(question.get("title").asText()) + .titleSlug(question.get("titleSlug").asText()) + .difficulty(question.get("difficulty").asText()) + .acceptanceRate((float) question.get("acRate").asDouble()) + .topics(tags) + .build()); } - }); + return result; + } catch (InterruptedException e) { + errorCounter().increment(); + Thread.currentThread().interrupt(); + throw new IllegalArgumentException("Thread interrupted", e); + } catch (Exception e) { + errorCounter().increment(); + throw new IllegalArgumentException("Error getting all problems from Leetcode API", e); + } } } diff --git a/src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java b/src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java index ae36f776f..54af49a65 100644 --- a/src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java +++ b/src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java @@ -1,8 +1,8 @@ package org.patinanetwork.codebloom.scheduled.auth; import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -51,7 +51,6 @@ public class LeetcodeAuthStealer { private final AuthRepository authRepository; private final Reporter reporter; private final Env env; - private final MeterRegistry meterRegistry; private final PlaywrightClient playwrightClient; public LeetcodeAuthStealer( @@ -65,69 +64,56 @@ public LeetcodeAuthStealer( this.authRepository = authRepository; this.reporter = reporter; this.env = env; - this.meterRegistry = meterRegistry; this.playwrightClient = playwrightClient; } - private Timer timer() { - var stackFrame = StackWalker.getInstance() - .walk(frames -> frames.skip(1).findFirst()) - .orElseThrow(); - - String methodName = stackFrame.getMethodName(); - String className = stackFrame.getClassName(); - - return meterRegistry.timer(METRIC_NAME, "class", className, "method", methodName); - } - /** * DO NOT RETURN THE TOKEN IN ANY API ENDPOINT.
This function utilizes Playwright in order to get an * authentication key from Leetcode. That code is stored in the database and can then be used to run authenticated * queries such as being used to retrieve code from our user submissions. */ @Scheduled(initialDelay = 0, fixedDelay = 1, timeUnit = TimeUnit.HOURS) + @Timed(value = METRIC_NAME) public void stealAuthCookie() { - timer().record(() -> { - boolean acquired = LOCK.writeLock().tryLock(); - if (!acquired) { - log.info("Lock failed to be acquired, bouncing..."); + boolean acquired = LOCK.writeLock().tryLock(); + if (!acquired) { + log.info("Lock failed to be acquired, bouncing..."); + return; + } + + try { + Auth mostRecentAuth = authRepository.getMostRecentAuth(); + + // The auth token should be refreshed every 4 hours. + if (mostRecentAuth != null + && mostRecentAuth + .getCreatedAt() + .isAfter(StandardizedOffsetDateTime.now().minus(4, ChronoUnit.HOURS))) { + log.info("Auth token already exists, using token from database."); + cookie = mostRecentAuth.getToken(); + csrf = mostRecentAuth.getCsrf(); return; } - try { - Auth mostRecentAuth = authRepository.getMostRecentAuth(); - - // The auth token should be refreshed every 4 hours. - if (mostRecentAuth != null - && mostRecentAuth - .getCreatedAt() - .isAfter(StandardizedOffsetDateTime.now().minus(4, ChronoUnit.HOURS))) { - log.info("Auth token already exists, using token from database."); - cookie = mostRecentAuth.getToken(); - csrf = mostRecentAuth.getCsrf(); - return; - } - - log.info("falling back to checking redis client..."); - Optional authToken = redisClient.getAuth(); - - log.info("auth token in redis = {}", authToken.isPresent()); - - if (authToken.isPresent()) { - log.info("auth token found in redis client"); - cookie = authToken.get(); - csrf = null; // don't care in ci. - return; - } - - log.info("auth token not found in redis client"); - log.info("Auth token is missing/expired. Attempting to receive token..."); - - stealCookieImpl(); - } finally { - LOCK.writeLock().unlock(); + log.info("falling back to checking redis client..."); + Optional authToken = redisClient.getAuth(); + + log.info("auth token in redis = {}", authToken.isPresent()); + + if (authToken.isPresent()) { + log.info("auth token found in redis client"); + cookie = authToken.get(); + csrf = null; // don't care in ci. + return; } - }); + + log.info("auth token not found in redis client"); + log.info("Auth token is missing/expired. Attempting to receive token..."); + + stealCookieImpl(); + } finally { + LOCK.writeLock().unlock(); + } } /** @@ -137,72 +123,68 @@ public void stealAuthCookie() { *

You may await the `CompletableFuture` and receive the brand new token, or call-and-forget. */ @Async + @Timed(value = METRIC_NAME) public CompletableFuture> reloadCookie() { - return timer().record(() -> { - boolean acquired = LOCK.writeLock().tryLock(); - if (!acquired) { - log.info("Lock failed to be acquired, bouncing..."); - return CompletableFuture.completedFuture(Optional.empty()); - } - - try { - return CompletableFuture.completedFuture(Optional.ofNullable(stealCookieImpl())); - } finally { - LOCK.writeLock().unlock(); - } - }); + boolean acquired = LOCK.writeLock().tryLock(); + if (!acquired) { + log.info("Lock failed to be acquired, bouncing..."); + return CompletableFuture.completedFuture(Optional.empty()); + } + + try { + return CompletableFuture.completedFuture(Optional.ofNullable(stealCookieImpl())); + } finally { + LOCK.writeLock().unlock(); + } } + @Timed(value = METRIC_NAME) public String getCookie() { - return timer().record(() -> { - LOCK.readLock().lock(); - try { - return cookie; - } finally { - LOCK.readLock().unlock(); - } - }); + LOCK.readLock().lock(); + try { + return cookie; + } finally { + LOCK.readLock().unlock(); + } } /** * It's fine if this is null for some requests; it isn't a requirement to fetch data from the GraphQL layer of * leetcode.com */ + @Timed(value = METRIC_NAME) public String getCsrf() { - return timer().record(() -> { - if (csrf == null && !reported) { - reported = true; - reporter.log( - "getCsrf", - Report.builder() - .environments(env.getActiveProfiles()) - .location(Location.BACKEND) - .data( - "CSRF token is missing inside of LeetcodeAuthStealer. This may be something to look into.") - .build()); - } - - return csrf; - }); + if (csrf == null && !reported) { + reported = true; + reporter.log( + "getCsrf", + Report.builder() + .environments(env.getActiveProfiles()) + .location(Location.BACKEND) + .data( + "CSRF token is missing inside of LeetcodeAuthStealer. This may be something to look into.") + .build()); + } + + return csrf; } + @Timed(value = METRIC_NAME) String stealCookieImpl() { - return timer().record(() -> { - Optional auth = playwrightClient.getLeetcodeCookie(githubUsername, githubPassword); - if (auth.isPresent()) { - var a = auth.get(); - this.csrf = a.getCsrf(); - this.cookie = a.getToken(); - redisClient.setAuth(a.getToken(), 4, ChronoUnit.HOURS); - log.info("auth token stored in redis"); - this.authRepository.createAuth(Auth.builder() - .csrf(a.getCsrf()) - .token(a.getToken()) - .createdAt(StandardizedOffsetDateTime.now()) - .build()); - return cookie; - } - return null; - }); + Optional auth = playwrightClient.getLeetcodeCookie(githubUsername, githubPassword); + if (auth.isPresent()) { + var a = auth.get(); + this.csrf = a.getCsrf(); + this.cookie = a.getToken(); + redisClient.setAuth(a.getToken(), 4, ChronoUnit.HOURS); + log.info("auth token stored in redis"); + this.authRepository.createAuth(Auth.builder() + .csrf(a.getCsrf()) + .token(a.getToken()) + .createdAt(StandardizedOffsetDateTime.now()) + .build()); + return cookie; + } + return null; } } diff --git a/src/test/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientTest.java b/src/test/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientTest.java index 86327ead9..e46b0eecc 100644 --- a/src/test/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientTest.java +++ b/src/test/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientTest.java @@ -53,23 +53,23 @@ void setup() { @Test void testFindQuestionBySlug() throws Exception { String responseJson = """ - { - "data": { - "question": { - "questionId": "42", - "title": "Trapping Rain Water", - "titleSlug": "trapping-rain-water", - "difficulty": "Hard", - "content": "

Given n non-negative integers...

", - "stats": "{\\"acRate\\":\\"49.4%\\"}", - "topicTags": [ - {"name": "Array", "slug": "array"}, - {"name": "Two Pointers", "slug": "two-pointers"} - ] - } - } + { + "data": { + "question": { + "questionId": "42", + "title": "Trapping Rain Water", + "titleSlug": "trapping-rain-water", + "difficulty": "Hard", + "content": "

Given n non-negative integers...

", + "stats": "{\\"acRate\\":\\"49.4%\\"}", + "topicTags": [ + {"name": "Array", "slug": "array"}, + {"name": "Two Pointers", "slug": "two-pointers"} + ] } - """; + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -93,10 +93,10 @@ void testFindQuestionBySlug() throws Exception { @ValueSource(ints = {500, 302, 403}) void testFindQuestionBySlugThrottledAndTriggersReloadCookie(int statusCode) throws Exception { String responseJson = """ - { - "data": {} - } - """; + { + "data": {} + } + """; when(httpResponse.statusCode()).thenReturn(statusCode); when(httpResponse.body()).thenReturn(responseJson); @@ -116,27 +116,27 @@ void testFindQuestionBySlugThrottledAndTriggersReloadCookie(int statusCode) thro @Test void testFindSubmissionsByUsername() throws Exception { String responseJson = """ - { - "data": { - "recentAcSubmissionList": [ - { - "id": "1234567", - "title": "Two Sum", - "titleSlug": "two-sum", - "timestamp": "1640995200", - "statusDisplay": "Accepted" - }, - { - "id": "1234568", - "title": "Add Two Numbers", - "titleSlug": "add-two-numbers", - "timestamp": "1640995300", - "statusDisplay": "Accepted" - } - ] + { + "data": { + "recentAcSubmissionList": [ + { + "id": "1234567", + "title": "Two Sum", + "titleSlug": "two-sum", + "timestamp": "1640995200", + "statusDisplay": "Accepted" + }, + { + "id": "1234568", + "title": "Add Two Numbers", + "titleSlug": "add-two-numbers", + "timestamp": "1640995300", + "statusDisplay": "Accepted" } - } - """; + ] + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -155,10 +155,10 @@ void testFindSubmissionsByUsername() throws Exception { @ValueSource(ints = {500, 302, 403}) void testFindSubmissionsByUsernameThrottledAndTriggersReloadCookie(int statusCode) throws Exception { String responseJson = """ - { - "data": {} - } - """; + { + "data": {} + } + """; when(httpResponse.statusCode()).thenReturn(statusCode); when(httpResponse.body()).thenReturn(responseJson); @@ -178,20 +178,20 @@ void testFindSubmissionsByUsernameThrottledAndTriggersReloadCookie(int statusCod @Test void testFindSubmissionsByUsernameWithLimit() throws Exception { String responseJson = """ - { - "data": { - "recentAcSubmissionList": [ - { - "id": "1234567", - "title": "Two Sum", - "titleSlug": "two-sum", - "timestamp": "1640995200", - "statusDisplay": "Accepted" - } - ] + { + "data": { + "recentAcSubmissionList": [ + { + "id": "1234567", + "title": "Two Sum", + "titleSlug": "two-sum", + "timestamp": "1640995200", + "statusDisplay": "Accepted" } - } - """; + ] + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -207,24 +207,24 @@ void testFindSubmissionsByUsernameWithLimit() throws Exception { @Test void testFindSubmissionDetailBySubmissionId() throws Exception { String responseJson = """ - { - "data": { - "submissionDetails": { - "runtime": 45, - "runtimeDisplay": "45 ms", - "runtimePercentile": 85.5, - "memory": 14000000, - "memoryDisplay": "14 MB", - "memoryPercentile": 72.3, - "code": "class Solution {}", - "lang": { - "name": "java", - "verboseName": "Java" - } - } + { + "data": { + "submissionDetails": { + "runtime": 45, + "runtimeDisplay": "45 ms", + "runtimePercentile": 85.5, + "memory": 14000000, + "memoryDisplay": "14 MB", + "memoryPercentile": 72.3, + "code": "class Solution {}", + "lang": { + "name": "java", + "verboseName": "Java" } } - """; + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -250,10 +250,10 @@ void testFindSubmissionDetailBySubmissionId() throws Exception { @ValueSource(ints = {500, 302, 403}) void testFindSubmissionDetailBySubmissionIdThrottledAndTriggersReloadCookie(int statusCode) throws Exception { String responseJson = """ - { - "data": {} - } - """; + { + "data": {} + } + """; when(httpResponse.statusCode()).thenReturn(statusCode); when(httpResponse.body()).thenReturn(responseJson); @@ -273,18 +273,18 @@ void testFindSubmissionDetailBySubmissionIdThrottledAndTriggersReloadCookie(int @Test void testGetPotd() throws Exception { String responseJson = """ - { - "data": { - "activeDailyCodingChallengeQuestion": { - "question": { - "titleSlug": "two-sum", - "title": "Two Sum", - "difficulty": "Easy" - } - } + { + "data": { + "activeDailyCodingChallengeQuestion": { + "question": { + "titleSlug": "two-sum", + "title": "Two Sum", + "difficulty": "Easy" } } - """; + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -303,10 +303,10 @@ void testGetPotd() throws Exception { @ValueSource(ints = {500, 302, 403}) void testGetPotdThrottledAndTriggersReloadCookie(int statusCode) throws Exception { String responseJson = """ - { - "data": {} - } - """; + { + "data": {} + } + """; when(httpResponse.statusCode()).thenReturn(statusCode); when(httpResponse.body()).thenReturn(responseJson); @@ -326,20 +326,20 @@ void testGetPotdThrottledAndTriggersReloadCookie(int statusCode) throws Exceptio @Test void testGetUserProfile() throws Exception { String responseJson = """ - { - "data": { - "matchedUser": { - "username": "testuser", - "profile": { - "ranking": "12345", - "userAvatar": "https://example.com/avatar.jpg", - "realName": "Test User", - "aboutMe": "Passionate coder" - } - } + { + "data": { + "matchedUser": { + "username": "testuser", + "profile": { + "ranking": "12345", + "userAvatar": "https://example.com/avatar.jpg", + "realName": "Test User", + "aboutMe": "Passionate coder" } } - """; + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -360,10 +360,10 @@ void testGetUserProfile() throws Exception { @ValueSource(ints = {500, 302, 403}) void testGetUserProfileThrottledAndTriggersReloadCookie(int statusCode) throws Exception { String responseJson = """ - { - "data": {} - } - """; + { + "data": {} + } + """; when(httpResponse.statusCode()).thenReturn(statusCode); when(httpResponse.body()).thenReturn(responseJson); @@ -383,27 +383,27 @@ void testGetUserProfileThrottledAndTriggersReloadCookie(int statusCode) throws E @Test void testGetAllTopicTags() throws Exception { String responseJson = """ - { - "data": { - "questionTopicTags": { - "edges": [ - { - "node": { - "name": "Array", - "slug": "array" - } - }, - { - "node": { - "name": "Hash Table", - "slug": "hash-table" - } - } - ] + { + "data": { + "questionTopicTags": { + "edges": [ + { + "node": { + "name": "Array", + "slug": "array" + } + }, + { + "node": { + "name": "Hash Table", + "slug": "hash-table" + } } - } + ] } - """; + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -420,10 +420,10 @@ void testGetAllTopicTags() throws Exception { @ValueSource(ints = {500, 302, 403}) void testGetAllTopicTagsThrottledAndTriggersReloadCookie(int statusCode) throws Exception { String responseJson = """ - { - "data": {} - } - """; + { + "data": {} + } + """; when(httpResponse.statusCode()).thenReturn(statusCode); when(httpResponse.body()).thenReturn(responseJson); @@ -443,36 +443,36 @@ void testGetAllTopicTagsThrottledAndTriggersReloadCookie(int statusCode) throws @Test void testGetAllProblems() throws Exception { String responseJson = """ - { - "data": { - "problemsetQuestionListV2": { - "questions": [ - { - "questionFrontendId": 1, - "title": "Two Sum", - "titleSlug": "two-sum", - "difficulty": "Easy", - "acRate": 49.4, - "topicTags": [ - {"name": "Array", "slug": "array"}, - {"name": "Hash Table", "slug": "hash-table"} - ] - }, - { - "questionFrontendId": 2, - "title": "Add Two Numbers", - "titleSlug": "add-two-numbers", - "difficulty": "Medium", - "acRate": 42.1, - "topicTags": [ - {"name": "Linked List", "slug": "linked-list"} - ] - } + { + "data": { + "problemsetQuestionListV2": { + "questions": [ + { + "questionFrontendId": 1, + "title": "Two Sum", + "titleSlug": "two-sum", + "difficulty": "Easy", + "acRate": 49.4, + "topicTags": [ + {"name": "Array", "slug": "array"}, + {"name": "Hash Table", "slug": "hash-table"} + ] + }, + { + "questionFrontendId": 2, + "title": "Add Two Numbers", + "titleSlug": "add-two-numbers", + "difficulty": "Medium", + "acRate": 42.1, + "topicTags": [ + {"name": "Linked List", "slug": "linked-list"} ] } - } + ] } - """; + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -490,23 +490,23 @@ void testGetAllProblems() throws Exception { @Test void testGetAllProblemsQuestionsIsNotArray() throws Exception { String responseJson = """ - { - "data": { - "problemsetQuestionListV2": { - "questions": { - "questionFrontendId": 2, - "title": "Add Two Numbers", - "titleSlug": "add-two-numbers", - "difficulty": "Medium", - "acRate": 42.1, - "topicTags": [ - {"name": "Linked List", "slug": "linked-list"} - ] - } - } + { + "data": { + "problemsetQuestionListV2": { + "questions": { + "questionFrontendId": 2, + "title": "Add Two Numbers", + "titleSlug": "add-two-numbers", + "difficulty": "Medium", + "acRate": 42.1, + "topicTags": [ + {"name": "Linked List", "slug": "linked-list"} + ] } } - """; + } + } + """; when(httpResponse.statusCode()).thenReturn(200); when(httpResponse.body()).thenReturn(responseJson); @@ -527,10 +527,10 @@ void testGetAllProblemsQuestionsIsNotArray() throws Exception { @ValueSource(ints = {500, 302, 403}) void testGetAllProblemsThrottledAndTriggersReloadCookie(int statusCode) throws Exception { String responseJson = """ - { - "data": {} - } - """; + { + "data": {} + } + """; when(httpResponse.statusCode()).thenReturn(statusCode); when(httpResponse.body()).thenReturn(responseJson); @@ -546,4 +546,42 @@ void testGetAllProblemsThrottledAndTriggersReloadCookie(int statusCode) throws E verify(leetcodeAuthStealer, times(1)).reloadCookie(); } } + + @Test + void testFindQuestionBySlugThrowsInterruptedException() throws Exception { + String responseJson = """ + { + "data": { + "question": { + "questionId": "42", + "title": "Trapping Rain Water", + "titleSlug": "trapping-rain-water", + "difficulty": "Hard", + "content": "

Given n non-negative integers...

", + "stats": "{\\"acRate\\":\\"49.4%\\"}", + "topicTags": [ + {"name": "Array", "slug": "array"}, + {"name": "Two Pointers", "slug": "two-pointers"} + ] + } + } + } + """; + + when(httpResponse.statusCode()).thenReturn(200); + when(httpResponse.body()).thenReturn(responseJson); + when(httpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenThrow(InterruptedException.class); + try { + leetcodeClient.findQuestionBySlug("trapping-rain-water"); + fail("Exception expected"); + } catch (RuntimeException e) { + assertNotNull(e); + assertNotNull(e.getCause()); + assertTrue(e.getMessage().contains("Thread interrupted")); + assertInstanceOf(InterruptedException.class, e.getCause()); + assertTrue(Thread.currentThread().isInterrupted()); + Thread.interrupted(); + } + } }