diff --git a/.editorconfig b/.editorconfig index 45e8641..65488a6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,16 @@ # Top-most EditorConfig file root = true +# use unix newline characters +end_of_line = lf + [*.cs] dotnet_naming_rule.methods_must_be_camel_case.severity = warning dotnet_naming_rule.methods_must_be_camel_case.symbols = private_methods dotnet_naming_rule.methods_must_be_camel_case.style = camel_case_style dotnet_naming_symbols.private_methods.applicable_kinds = method -dotnet_naming_symbols.private_methods.applicable_accessibilities = private +dotnet_naming_symbols.private_methods.applicable_accessibilities = private dotnet_naming_style.camel_case_style.capitalization = camel_case dotnet_diagnostic.CA2254.severity = none @@ -199,5 +202,3 @@ indent_size = 2 # Shell scripts [*.sh] end_of_line = lf -[*.{cmd,bat}] -end_of_line = crlf \ No newline at end of file diff --git a/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs b/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs new file mode 100644 index 0000000..825bbdf --- /dev/null +++ b/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs @@ -0,0 +1,219 @@ +using System.Text; +using LocalModels; + +public class QuizDeterministicChecks +{ + + [Test] + public void SerializationIsDeterministic_EmptyQuiz() + { + var quiz = new LocalQuiz() + { + Name = "Test Quiz", + Description = "quiz description", + LockAt = new DateTime(2022, 10, 3, 12, 5, 0), + DueAt = new DateTime(2022, 10, 3, 12, 5, 0), + ShuffleAnswers = true, + OneQuestionAtATime = true, + LocalAssignmentGroupName = "Assignments" + }; + var quizMarkdown = quiz.ToMarkdown(); + + var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); + parsedQuiz.Should().BeEquivalentTo(quiz); + } + + [Test] + public void SerializationIsDeterministic_ShowCorrectAnswers() + { + var quiz = new LocalQuiz() + { + Name = "Test Quiz", + Description = "quiz description", + LockAt = new DateTime(2022, 10, 3, 12, 5, 0), + DueAt = new DateTime(2022, 10, 3, 12, 5, 0), + showCorrectAnswers = false, + ShuffleAnswers = true, + OneQuestionAtATime = true, + LocalAssignmentGroupName = "Assignments" + }; + var quizMarkdown = quiz.ToMarkdown(); + + var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); + parsedQuiz.Should().BeEquivalentTo(quiz); + } + + [Test] + public void SerializationIsDeterministic_ShortAnswer() + { + var quiz = new LocalQuiz() + { + Name = "Test Quiz", + Description = "quiz description", + LockAt = new DateTime(2022, 10, 3, 12, 5, 0), + DueAt = new DateTime(2022, 10, 3, 12, 5, 0), + ShuffleAnswers = true, + OneQuestionAtATime = true, + LocalAssignmentGroupName = "Assignments", + Questions = new LocalQuizQuestion[] + { + new () + { + Text = "test short answer", + QuestionType = QuestionType.SHORT_ANSWER, + Points = 1 + } + } + }; + var quizMarkdown = quiz.ToMarkdown(); + + var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); + parsedQuiz.Should().BeEquivalentTo(quiz); + } + + [Test] + public void SerializationIsDeterministic_Essay() + { + var quiz = new LocalQuiz() + { + Name = "Test Quiz", + Description = "quiz description", + LockAt = new DateTime(2022, 10, 3, 12, 5, 0), + DueAt = new DateTime(2022, 10, 3, 12, 5, 0), + ShuffleAnswers = true, + OneQuestionAtATime = true, + LocalAssignmentGroupName = "Assignments", + Questions = new LocalQuizQuestion[] + { + new () + { + Text = "test essay", + QuestionType = QuestionType.ESSAY, + Points = 1 + } + } + }; + var quizMarkdown = quiz.ToMarkdown(); + + var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); + parsedQuiz.Should().BeEquivalentTo(quiz); + } + + [Test] + public void SerializationIsDeterministic_MultipleAnswer() + { + var quiz = new LocalQuiz() + { + Name = "Test Quiz", + Description = "quiz description", + LockAt = new DateTime(2022, 10, 3, 12, 5, 0), + DueAt = new DateTime(2022, 10, 3, 12, 5, 0), + ShuffleAnswers = true, + OneQuestionAtATime = true, + LocalAssignmentGroupName = "Assignments", + Questions = new LocalQuizQuestion[] + { + new () + { + Text = "test multiple answer", + QuestionType = QuestionType.MULTIPLE_ANSWERS, + Points = 1, + Answers = new LocalQuizQuestionAnswer[] + { + new() { + Correct = true, + Text="yes", + }, + new() { + Correct = true, + Text="no", + } + } + } + } + }; + var quizMarkdown = quiz.ToMarkdown(); + + var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); + parsedQuiz.Should().BeEquivalentTo(quiz); + } + + [Test] + public void SerializationIsDeterministic_MultipleChoice() + { + var quiz = new LocalQuiz() + { + Name = "Test Quiz", + Description = "quiz description", + LockAt = new DateTime(2022, 10, 3, 12, 5, 0), + DueAt = new DateTime(2022, 10, 3, 12, 5, 0), + ShuffleAnswers = true, + OneQuestionAtATime = true, + LocalAssignmentGroupName = "Assignments", + Questions = new LocalQuizQuestion[] + { + new () + { + Text = "test multiple choice", + QuestionType = QuestionType.MULTIPLE_CHOICE, + Points = 1, + Answers = new LocalQuizQuestionAnswer[] + { + new() { + Correct = true, + Text="yes", + }, + new() { + Correct = false, + Text="no", + } + } + } + } + }; + var quizMarkdown = quiz.ToMarkdown(); + + var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); + parsedQuiz.Should().BeEquivalentTo(quiz); + } + [Test] + public void SerializationIsDeterministic_Matching() + { + var quiz = new LocalQuiz() + { + Name = "Test Quiz", + Description = "quiz description", + LockAt = new DateTime(2022, 10, 3, 12, 5, 0), + DueAt = new DateTime(2022, 10, 3, 12, 5, 0), + ShuffleAnswers = true, + OneQuestionAtATime = true, + LocalAssignmentGroupName = "Assignments", + Questions = new LocalQuizQuestion[] + { + new () + { + Text = "test matching", + QuestionType = QuestionType.MATCHING, + Points = 1, + Answers = new LocalQuizQuestionAnswer[] + { + new() { + Correct = true, + Text="yes", + MatchedText = "testing yes" + }, + new() { + Correct = true, + Text="no", + MatchedText = "testing no" + } + } + } + } + }; + var quizMarkdown = quiz.ToMarkdown(); + + var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); + parsedQuiz.Should().BeEquivalentTo(quiz); + } +} diff --git a/Management.Test/Markdown/Quiz/QuizMarkdownTests.cs b/Management.Test/Markdown/Quiz/QuizMarkdownTests.cs index 2a0e036..c2616e3 100644 --- a/Management.Test/Markdown/Quiz/QuizMarkdownTests.cs +++ b/Management.Test/Markdown/Quiz/QuizMarkdownTests.cs @@ -1,3 +1,4 @@ +using System.Text; using LocalModels; // try to follow syntax from https://github.com/gpoore/text2qti @@ -22,7 +23,7 @@ this is my description in markdown OneQuestionAtATime = false, LocalAssignmentGroupName = "someId", AllowedAttempts = -1, - Questions = new LocalQuizQuestion[] { } + Questions = [] }; var markdown = quiz.ToMarkdown(); @@ -39,28 +40,82 @@ this is my description in markdown [Test] public void TestCanParseMarkdownQuizWithNoQuestions() { - var rawMarkdownQuiz = @" -Name: Test Quiz -ShuffleAnswers: true -OneQuestionAtATime: false -DueAt: 2023-08-21T23:59:00 -LockAt: 2023-08-21T23:59:00 -AssignmentGroup: Assignments -AllowedAttempts: -1 -Description: this is the -multi line -description ---- -"; - var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz); + var rawMarkdownQuiz = new StringBuilder(); + rawMarkdownQuiz.Append("Name: Test Quiz\n"); + rawMarkdownQuiz.Append("ShuffleAnswers: true\n"); + rawMarkdownQuiz.Append("OneQuestionAtATime: false\n"); + rawMarkdownQuiz.Append("DueAt: 2023-08-21T23:59:00\n"); + rawMarkdownQuiz.Append("LockAt: 2023-08-21T23:59:00\n"); + rawMarkdownQuiz.Append("AssignmentGroup: Assignments\n"); + rawMarkdownQuiz.Append("AllowedAttempts: -1\n"); + rawMarkdownQuiz.Append("Description: this is the\n"); + rawMarkdownQuiz.Append("multi line\n"); + rawMarkdownQuiz.Append("description\n"); + rawMarkdownQuiz.Append("---\n"); + rawMarkdownQuiz.Append('\n'); + + var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz.ToString()); + + + var expectedDescription = new StringBuilder(); + expectedDescription.Append("this is the\n"); + expectedDescription.Append("multi line\n"); + expectedDescription.Append("description"); quiz.Name.Should().Be("Test Quiz"); quiz.ShuffleAnswers.Should().Be(true); quiz.OneQuestionAtATime.Should().BeFalse(); quiz.AllowedAttempts.Should().Be(-1); - quiz.Description.Should().Be(@"this is the -multi line -description"); + quiz.Description.Should().Be(expectedDescription.ToString()); + } + [Test] + public void TestCanParseMarkdownQuizPassword() + { + + var password = "this-is-the-password"; + var rawMarkdownQuiz = new StringBuilder(); + rawMarkdownQuiz.Append("Name: Test Quiz\n"); + rawMarkdownQuiz.Append($"Password: {password}\n"); + rawMarkdownQuiz.Append("ShuffleAnswers: true\n"); + rawMarkdownQuiz.Append("OneQuestionAtATime: false\n"); + rawMarkdownQuiz.Append("DueAt: 2023-08-21T23:59:00\n"); + rawMarkdownQuiz.Append("LockAt: 2023-08-21T23:59:00\n"); + rawMarkdownQuiz.Append("AssignmentGroup: Assignments\n"); + rawMarkdownQuiz.Append("AllowedAttempts: -1\n"); + rawMarkdownQuiz.Append("Description: this is the\n"); + rawMarkdownQuiz.Append("multi line\n"); + rawMarkdownQuiz.Append("description\n"); + rawMarkdownQuiz.Append("---\n"); + rawMarkdownQuiz.Append('\n'); + + var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz.ToString()); + + + quiz.Password.Should().Be(password); + } + + [Test] + public void TestCanParseMarkdownQuiz_CanConfigureToShowCorrectAnswers() + { + var rawMarkdownQuiz = new StringBuilder(); + rawMarkdownQuiz.Append("Name: Test Quiz\n"); + rawMarkdownQuiz.Append("ShuffleAnswers: true\n"); + rawMarkdownQuiz.Append("OneQuestionAtATime: false\n"); + rawMarkdownQuiz.Append("ShowCorrectAnswers: false\n"); + rawMarkdownQuiz.Append("DueAt: 2023-08-21T23:59:00\n"); + rawMarkdownQuiz.Append("LockAt: 2023-08-21T23:59:00\n"); + rawMarkdownQuiz.Append("AssignmentGroup: Assignments\n"); + rawMarkdownQuiz.Append("AllowedAttempts: -1\n"); + rawMarkdownQuiz.Append("Description: this is the\n"); + rawMarkdownQuiz.Append("multi line\n"); + rawMarkdownQuiz.Append("description\n"); + rawMarkdownQuiz.Append("---\n"); + rawMarkdownQuiz.Append('\n'); + + var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz.ToString()); + + + quiz.showCorrectAnswers.Should().BeFalse(); } [Test] @@ -74,14 +129,14 @@ DueAt: 2023-08-21T23:59:00 LockAt: 2023-08-21T23:59:00 AssignmentGroup: Assignments AllowedAttempts: -1 -Description: this is the +Description: this is the multi line description --- Points: 2 `some type` of question -with many +with many ``` lines @@ -89,7 +144,7 @@ lines *a) true b) false - + endline"; var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz); @@ -115,7 +170,7 @@ DueAt: 2023-08-21T23:59:00 LockAt: 2023-08-21T23:59:00 AssignmentGroup: Assignments AllowedAttempts: -1 -Description: this is the +Description: this is the multi line description --- @@ -148,7 +203,7 @@ DueAt: 2023-08-21T23:59:00 LockAt: 2023-08-21T23:59:00 AssignmentGroup: Assignments AllowedAttempts: -1 -Description: this is the +Description: this is the multi line description --- @@ -165,196 +220,4 @@ Which events are triggered when the user clicks on an input field? short_answer"; questionMarkdown.Should().Contain(expectedMarkdown); } - - [Test] - public void SerializationIsDeterministic_EmptyQuiz() - { - var quiz = new LocalQuiz() - { - Name = "Test Quiz", - Description = "quiz description", - LockAt = new DateTime(2022, 10, 3, 12, 5, 0), - DueAt = new DateTime(2022, 10, 3, 12, 5, 0), - ShuffleAnswers = true, - OneQuestionAtATime = true, - LocalAssignmentGroupName = "Assignments" - }; - var quizMarkdown = quiz.ToMarkdown(); - - var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); - parsedQuiz.Should().BeEquivalentTo(quiz); - } - [Test] - public void SerializationIsDeterministic_ShortAnswer() - { - var quiz = new LocalQuiz() - { - Name = "Test Quiz", - Description = "quiz description", - LockAt = new DateTime(2022, 10, 3, 12, 5, 0), - DueAt = new DateTime(2022, 10, 3, 12, 5, 0), - ShuffleAnswers = true, - OneQuestionAtATime = true, - LocalAssignmentGroupName = "Assignments", - Questions = new LocalQuizQuestion[] - { - new () - { - Text = "test short answer", - QuestionType = QuestionType.SHORT_ANSWER, - Points = 1 - } - } - }; - var quizMarkdown = quiz.ToMarkdown(); - - var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); - parsedQuiz.Should().BeEquivalentTo(quiz); - } - - [Test] - public void SerializationIsDeterministic_Essay() - { - var quiz = new LocalQuiz() - { - Name = "Test Quiz", - Description = "quiz description", - LockAt = new DateTime(2022, 10, 3, 12, 5, 0), - DueAt = new DateTime(2022, 10, 3, 12, 5, 0), - ShuffleAnswers = true, - OneQuestionAtATime = true, - LocalAssignmentGroupName = "Assignments", - Questions = new LocalQuizQuestion[] - { - new () - { - Text = "test essay", - QuestionType = QuestionType.ESSAY, - Points = 1 - } - } - }; - var quizMarkdown = quiz.ToMarkdown(); - - var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); - parsedQuiz.Should().BeEquivalentTo(quiz); - } - - [Test] - public void SerializationIsDeterministic_MultipleAnswer() - { - var quiz = new LocalQuiz() - { - Name = "Test Quiz", - Description = "quiz description", - LockAt = new DateTime(2022, 10, 3, 12, 5, 0), - DueAt = new DateTime(2022, 10, 3, 12, 5, 0), - ShuffleAnswers = true, - OneQuestionAtATime = true, - LocalAssignmentGroupName = "Assignments", - Questions = new LocalQuizQuestion[] - { - new () - { - Text = "test multiple answer", - QuestionType = QuestionType.MULTIPLE_ANSWERS, - Points = 1, - Answers = new LocalQuizQuestionAnswer[] - { - new() { - Correct = true, - Text="yes", - }, - new() { - Correct = true, - Text="no", - } - } - } - } - }; - var quizMarkdown = quiz.ToMarkdown(); - - var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); - parsedQuiz.Should().BeEquivalentTo(quiz); - } - - [Test] - public void SerializationIsDeterministic_MultipleChoice() - { - var quiz = new LocalQuiz() - { - Name = "Test Quiz", - Description = "quiz description", - LockAt = new DateTime(2022, 10, 3, 12, 5, 0), - DueAt = new DateTime(2022, 10, 3, 12, 5, 0), - ShuffleAnswers = true, - OneQuestionAtATime = true, - LocalAssignmentGroupName = "Assignments", - Questions = new LocalQuizQuestion[] - { - new () - { - Text = "test multiple choice", - QuestionType = QuestionType.MULTIPLE_CHOICE, - Points = 1, - Answers = new LocalQuizQuestionAnswer[] - { - new() { - Correct = true, - Text="yes", - }, - new() { - Correct = false, - Text="no", - } - } - } - } - }; - var quizMarkdown = quiz.ToMarkdown(); - - var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); - parsedQuiz.Should().BeEquivalentTo(quiz); - } - [Test] - public void SerializationIsDeterministic_Matching() - { - var quiz = new LocalQuiz() - { - Name = "Test Quiz", - Description = "quiz description", - LockAt = new DateTime(2022, 10, 3, 12, 5, 0), - DueAt = new DateTime(2022, 10, 3, 12, 5, 0), - ShuffleAnswers = true, - OneQuestionAtATime = true, - LocalAssignmentGroupName = "Assignments", - Questions = new LocalQuizQuestion[] - { - new () - { - Text = "test matching", - QuestionType = QuestionType.MATCHING, - Points = 1, - Answers = new LocalQuizQuestionAnswer[] - { - new() { - Correct = true, - Text="yes", - MatchedText = "testing yes" - }, - new() { - Correct = true, - Text="no", - MatchedText = "testing no" - } - } - } - } - }; - var quizMarkdown = quiz.ToMarkdown(); - - var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); - parsedQuiz.Should().BeEquivalentTo(quiz); - } -} \ No newline at end of file +} diff --git a/Management/Models/Local/Quiz/LocalQuiz.cs b/Management/Models/Local/Quiz/LocalQuiz.cs index 8deb817..2295b3a 100644 --- a/Management/Models/Local/Quiz/LocalQuiz.cs +++ b/Management/Models/Local/Quiz/LocalQuiz.cs @@ -5,11 +5,14 @@ namespace LocalModels; public record LocalQuiz: IModuleItem { + public required string Name { get; init; } public required string Description { get; init; } + public string? Password { get; init; } = null; public DateTime? LockAt { get; init; } public DateTime DueAt { get; init; } public bool ShuffleAnswers { get; init; } = true; + public bool showCorrectAnswers { get; init; } = true; public bool OneQuestionAtATime { get; init; } = false; public string? LocalAssignmentGroupName { get; init; } public int AllowedAttempts { get; init; } = -1; // -1 is infinite @@ -21,6 +24,7 @@ public record LocalQuiz: IModuleItem // If “until_after_last_attempt”, students can only see results after their last attempt. (Only valid if allowed_attempts > 1). Defaults to null. public IEnumerable Questions { get; init; } = Enumerable.Empty(); + public ulong? GetCanvasAssignmentGroupId(IEnumerable assignmentGroups) => assignmentGroups .FirstOrDefault(g => g.Name == LocalAssignmentGroupName)? @@ -44,7 +48,9 @@ public record LocalQuiz: IModuleItem return $@"Name: {Name} LockAt: {LockAt} DueAt: {DueAt} +Password: {Password} ShuffleAnswers: {ShuffleAnswers.ToString().ToLower()} +ShowCorrectAnswers: {showCorrectAnswers.ToString().ToLower()} OneQuestionAtATime: {OneQuestionAtATime.ToString().ToLower()} AssignmentGroup: {LocalAssignmentGroupName} AllowedAttempts: {AllowedAttempts} @@ -82,6 +88,18 @@ Description: {Description} : throw new QuizMarkdownParseException($"Error with ShuffleAnswers: {rawShuffleAnswers}"); + var rawPassword = extractLabelValue(settings, "Password"); + var password = rawPassword == null || rawPassword.Trim() == string.Empty + ? null + : rawPassword; + + + var rawShowCorrectAnswers = extractLabelValue(settings, "ShowCorrectAnswers"); + var showCorrectAnswers = bool.TryParse(rawShowCorrectAnswers, out bool parsedShowCorrectAnswers) + ? parsedShowCorrectAnswers + : true; //default to true + + var rawOneQuestionAtATime = extractLabelValue(settings, "OneQuestionAtATime"); var oneQuestionAtATime = bool.TryParse(rawOneQuestionAtATime, out bool parsedOneQuestion) ? parsedOneQuestion @@ -112,9 +130,11 @@ Description: {Description} { Name = name, Description = description, + Password = password, LockAt = lockAt, DueAt = dueAt, ShuffleAnswers = shuffleAnswers, + showCorrectAnswers = showCorrectAnswers, OneQuestionAtATime = oneQuestionAtATime, LocalAssignmentGroupName = assignmentGroup, AllowedAttempts = allowedAttempts, diff --git a/Management/Services/Canvas/CanvasAssignmentGroupService.cs b/Management/Services/Canvas/CanvasAssignmentGroupService.cs index d0f6267..17b3670 100644 --- a/Management/Services/Canvas/CanvasAssignmentGroupService.cs +++ b/Management/Services/Canvas/CanvasAssignmentGroupService.cs @@ -11,7 +11,7 @@ public class CanvasAssignmentGroupService private readonly MyLogger logger; public CanvasAssignmentGroupService( - IWebRequestor webRequestor, + IWebRequestor webRequestor, CanvasServiceUtils utils, MyLogger logger ) @@ -73,4 +73,4 @@ public class CanvasAssignmentGroupService await webRequestor.PutAsync(request); } -} \ No newline at end of file +} diff --git a/Management/Services/Canvas/CanvasQuizService.cs b/Management/Services/Canvas/CanvasQuizService.cs index 11018df..c9cbc44 100644 --- a/Management/Services/Canvas/CanvasQuizService.cs +++ b/Management/Services/Canvas/CanvasQuizService.cs @@ -50,6 +50,8 @@ public class CanvasQuizService( // assignment_group_id = "quiz", // TODO: support specific assignment groups // time_limit = localQuiz.TimeLimit, shuffle_answers = localQuiz.ShuffleAnswers, + access_code = localQuiz.Password, + show_correct_answers = localQuiz.showCorrectAnswers, // hide_results = localQuiz.HideResults, allowed_attempts = localQuiz.AllowedAttempts, one_question_at_a_time = false, @@ -91,11 +93,9 @@ public class CanvasQuizService( private async Task hackFixRedundantAssignments(ulong canvasCourseId) { - using var activity = DiagnosticsConfig.Source.StartActivity("hack fixing redundant quiz assignments that are auto-created"); activity?.SetTag("canvas syncronization", true); - var canvasAssignments = await assignments.GetAll(canvasCourseId); var assignmentsToDelete = canvasAssignments .Where( @@ -113,7 +113,7 @@ public class CanvasQuizService( a.Name ); } - ); + ).ToArray(); await Task.WhenAll(tasks); } diff --git a/Management/Services/WebRequestor.cs b/Management/Services/WebRequestor.cs index 325db51..6016c91 100644 --- a/Management/Services/WebRequestor.cs +++ b/Management/Services/WebRequestor.cs @@ -9,6 +9,8 @@ public class WebRequestor : IWebRequestor private RestClient client; private readonly IConfiguration _config; private readonly ILogger logger; + private static int rateLimitRetryCount = 6; + private static int rateLimitSleepInterval = 1000; public WebRequestor(IConfiguration config, ILogger logger) { @@ -34,39 +36,38 @@ public class WebRequestor : IWebRequestor return (deserialize(response), response); } - public async Task PostAsync(RestRequest request) - { - using var activity = DiagnosticsConfig.Source.StartActivity("sending post"); - activity?.AddTag("success", false); + public async Task PostAsync(RestRequest request) => await rateLimitAwarePostAsync(request, 0); + + private async Task rateLimitAwarePostAsync(RestRequest request, int retryCount = 0) + { request.AddHeader("Content-Type", "application/json"); - try - { - var response = await client.ExecutePostAsync(request); - activity?.AddTag("url", response.ResponseUri); + var response = await client.ExecutePostAsync(request); - if (isRateLimited(response)) - logger.LogInformation("hit rate limit"); - - if (!response.IsSuccessful) - { - logger.LogError($"Error with response, response content: {response.Content}", response); - throw new Exception("error with response"); - } - activity?.AddTag("success", true); - return response; + if (isRateLimited(response)) { + if(retryCount < rateLimitRetryCount){ + logger.LogInformation($"hit rate limit on post, retry count is {retryCount} / {rateLimitRetryCount}, retrying"); + Console.WriteLine($"hit rate limit on post, retry count is {retryCount} / {rateLimitRetryCount}, retrying"); + Thread.Sleep(rateLimitSleepInterval); + return await rateLimitAwarePostAsync(request, retryCount + 1);} } - catch (Exception e) - { - Console.WriteLine("inside post catch block"); - throw e; + if (!response.IsSuccessful) + { + logger.LogError($"Error with response, response content: {response.Content}"); + throw new Exception($"error post response, retrycount: {retryCount}, ratelimited: {isRateLimited(response)}, code: {response.StatusCode}, response content: {response.Content}"); } + return response; } - private static bool isRateLimited(RestResponse response) => - response.StatusCode == HttpStatusCode.Forbidden && response.Content?.Contains("403 Forbidden (Rate Limit Exceeded)") != null; + private static bool isRateLimited(RestResponse response) + { + if(response.Content == null) + return false; + return response.StatusCode == HttpStatusCode.Forbidden + && response.Content.Contains("403 Forbidden (Rate Limit Exceeded)"); + } public async Task<(T?, RestResponse)> PostAsync(RestRequest request) { @@ -95,32 +96,33 @@ public class WebRequestor : IWebRequestor return (deserialize(response), response); } - public async Task DeleteAsync(RestRequest request) + public Task DeleteAsync(RestRequest request) => recursiveDeleteAsync(request, 0); + private async Task recursiveDeleteAsync(RestRequest request, int retryCount) { - // using var activity = DiagnosticsConfig.Source.StartActivity($"sending delete web request"); - // activity?.AddTag("success", false); - try { - var response = await client.DeleteAsync(request); if (isRateLimited(response)) Console.WriteLine("after delete response in rate limited"); - // Console.WriteLine(response.Content); - // activity?.AddTag("url", response.ResponseUri); - // activity?.AddTag("success", true); return response; - } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Forbidden) // && response.Content == "403 Forbidden (Rate Limit Exceeded)" - logger.LogInformation("hit rate limit in delete"); - - Console.WriteLine(e.StatusCode); - // Console.WriteLine(); - throw e; - + { + if(retryCount < rateLimitRetryCount) + { + logger.LogInformation($"hit rate limit in delete, retry count is {retryCount} / {rateLimitRetryCount}, retrying"); + Console.WriteLine($"hit rate limit in delete, retry count is {retryCount} / {rateLimitRetryCount}, retrying"); + Thread.Sleep(rateLimitSleepInterval); + return await recursiveDeleteAsync(request, retryCount + 1); + } + else + { + logger.LogInformation($"hit rate limit in delete, {rateLimitRetryCount} retries did not fix it"); + } + } + throw; } } @@ -164,4 +166,5 @@ public class WebRequestor : IWebRequestor throw; } } + } diff --git a/docker-compose.yml b/docker-compose.yml index 59f78e0..c5194df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: collector: image: otel/opentelemetry-collector-contrib volumes: - - ./ops/otel-collector-config.yml:/etc/otelcol-contrib/config.yaml + - ./canvas-development/otel-collector-config.yml:/etc/otelcol-contrib/config.yaml ports: - 1888:1888 # pprof extension - 8888:8888 # Prometheus metrics exposed by the Collector