From 5205d36af0126e6a35f9b690d97a1ef8b63f3f1a Mon Sep 17 00:00:00 2001 From: Jay Lorch Date: Sat, 9 Feb 2019 22:07:33 -0800 Subject: [PATCH 1/3] Update list of off-limits buildings in PH20 rules --- ServerCore/Pages/Resources/PH20/RulesPartial.cshtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ServerCore/Pages/Resources/PH20/RulesPartial.cshtml b/ServerCore/Pages/Resources/PH20/RulesPartial.cshtml index 894d27c5..cdb60e7e 100644 --- a/ServerCore/Pages/Resources/PH20/RulesPartial.cshtml +++ b/ServerCore/Pages/Resources/PH20/RulesPartial.cshtml @@ -38,7 +38,7 @@ security to be concerned. You must not enter any person's private office during the course of the hunt unless it is the office of a member of your team.

  • If any travel is required, it will take place on Microsoft property in or near East Campus, West Campus, North Campus, and/or RedWest. In other words, east of 148th Avenue NE, north of NE 24th Street, northwest of Bel-Red Road, and south of NE 60th Street. Some of these zones may have more activity than others. You are strongly encouraged to have a conference room in this general area.

  • - Within the area described above, buildings 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 24, 30, 50, 87, 111, 120, Redwest D, Redwest E, Studio A, Studio B, Studio C, and Studio X are off limits and should not be entered, other than to go your private office if it is located in one of these buildings.

    + Within the area described above, buildings 1, 2, 6, 8, 9, 10, 11, 11A, 22, 24, 26, 30, 33, 34, 50, 87, 111, 114, 120, 123, 124, 126, 127, Studio A, Studio B, and Studio C are off limits and should not be entered, other than to go your private office if it is located in one of these buildings.

    Cafeterias adjacent to a restricted or off-limits building are fair game, as are other areas on the main campus, west campus, and north campus. This list may change from hunt to hunt.

    If you believe a puzzle instructs you to go to an off-limits or restricted building, you've made a mistake. @@ -58,4 +58,4 @@ Teams should clean up their conference rooms after the end of the hunt. The regular cleaning schedule does not cover weekends. You should leave the room in an appropriate condition to be used for a Monday morning meeting.

    - \ No newline at end of file + From a3c1bb945ecd1a70b22bbeb080a31986ba7a21a9 Mon Sep 17 00:00:00 2001 From: Jay Lorch Date: Wed, 24 Apr 2019 19:48:30 -0700 Subject: [PATCH 2/3] Avoid annotation exceptions --- ServerCore/SyncController.cs | 160 +++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 65 deletions(-) diff --git a/ServerCore/SyncController.cs b/ServerCore/SyncController.cs index 36719078..81c050d6 100755 --- a/ServerCore/SyncController.cs +++ b/ServerCore/SyncController.cs @@ -288,87 +288,117 @@ private async Task HandleSyncAspectsRequiringListOfSolvedPuzzles(DecodedSyncRequ } ///

    - /// This routine stores in the database any annotations the requester has uploaded. + /// This routine updates a single existing annotation. As we do so, we increment + /// its version number and update its timestamp. + /// + /// This routine is only meant to be called when we know an annotation exists + /// with the given puzzle ID, team ID, and key. In other words, this routine + /// is called when we need to update that annotation. /// - private async Task StoreAnnotations(DecodedSyncRequest request, SyncResponse response, int puzzleId, int teamId) + private async Task UpdateOneAnnotation(SyncResponse response, int puzzleId, int teamId, int key, string contents) { - if (request.AnnotationRequests == null) { - return; - } + // You may wonder why we're using ExecuteSqlCommandAsync instead of "normal" + // Entity Framework database functions. The answer is that we need to atomically + // update the Version field of the record, and Entity Framework has no way of + // expressing that directly. + // + // The reason we want to update the version number atomically is that we rely + // on the version number being a unique identifier of an annotation. We don't + // want the following scenario: + // + // Alice tries to set the annotation for key 17 to A, and simultaneously Bob + // tries to set it to B. Each reads the current version number, finds it to be + // 3, and updates the annotation to have version 4. Both of these updates may + // succeed, but one will overwrite the other; let's say Bob's write happens last + // and "wins". So Alice may believe that version 4 is A when actually version 4 + // is B. When Alice asks for the current version, she'll be told it's version 4, + // and Alice will believe this means it's A. So Alice will believe that A is + // what's stored in the database even though it's not. Alice and Bob's computers + // will display different annotations for the same key, indefinitely. + // + // Note that we need a version number because the timestamp isn't guaranteed to + // be unique. So in the example above Alice and Bob might wind up updating with + // the same timestamp. + // + // You may also wonder why we use DateTime.Now instead of letting the database + // assign the timestamp itself. The reason is that the database might be running + // on a different machine than the puzzle server, and it might be using a different + // time zone. + + try { + var sqlCommand = "UPDATE Annotations SET Version = Version + 1, Contents = @Contents, Timestamp = @Timestamp WHERE PuzzleID = @PuzzleID AND TeamID = @TeamID AND [Key] = @Key"; + int result = await context.Database.ExecuteSqlCommandAsync(sqlCommand, + new SqlParameter("@Contents", contents), + new SqlParameter("@Timestamp", DateTime.Now), + new SqlParameter("@PuzzleID", puzzleId), + new SqlParameter("@TeamID", teamId), + new SqlParameter("@Key", key)); + if (result != 1) { + response.AddError("Annotation update failed."); + } + } + catch (DbUpdateException) { + response.AddError("Encountered error while trying to update annotation."); + } + catch (Exception) { + response.AddError("Miscellaneous error while trying to update annotation."); + } + } - foreach (var annotationRequest in request.AnnotationRequests) { - // Try to generate this as a new annotation, with version 1. + /// + /// This routine takes a single annotation that the requester has uploaded, and inserts + /// it if it doesn't exist and updates it otherwise. + /// + private async Task InsertOrUpdateOneAnnotation(SyncResponse response, int puzzleId, int teamId, int key, string contents) + { + // Check to see if the annotation already exists. If so, update it and we're done. - Annotation annotation = new Annotation(); - annotation.PuzzleID = puzzleId; - annotation.TeamID = teamId; - annotation.Key = annotationRequest.key; - annotation.Version = 1; - annotation.Contents = annotationRequest.contents; - annotation.Timestamp = DateTime.Now; + Annotation existingAnnotation = + await context.Annotations.SingleOrDefaultAsync(a => a.PuzzleID == puzzleId && a.TeamID == teamId && a.Key == key); + if (existingAnnotation != null) + { + context.Entry(existingAnnotation).State = EntityState.Detached; + UpdateOneAnnotation(response, puzzleId, teamId, key, contents); + } + else + { + // The annotation doesn't exist yet. So, try to generate a new annotation, with version 1. + Annotation annotation = new Annotation { PuzzleID = puzzleId, TeamID = teamId, Key = key, + Version = 1, Contents = contents, Timestamp = DateTime.Now }; try { context.Annotations.Add(annotation); await context.SaveChangesAsync(); } catch (DbUpdateException) { // If the insert fails, there must already be an annotation there with the - // same puzzle ID, team ID, and key. So we need to update the existing one. - // As we do so, we increment its version number and update its timestamp. - // - // You may wonder why we're using ExecuteSqlCommandAsync instead of "normal" - // Entity Framework database functions. The answer is that we need to atomically - // update the Version field of the record, and Entity Framework has no way of - // expressing that directly. - // - // The reason we want to update the version number atomically is that we rely - // on the version number being a unique identifier of an annotation. We don't - // want the following scenario: - // - // Alice tries to set the annotation for key 17 to A, and simultaneously Bob - // tries to set it to B. Each reads the current version number, finds it to be - // 3, and updates the annotation to have version 4. Both of these updates may - // succeed, but one will overwrite the other; let's say Bob's write happens last - // and "wins". So Alice may believe that version 4 is A when actually version 4 - // is B. When Alice asks for the current version, she'll be told it's version 4, - // and Alice will believe this means it's A. So Alice will believe that A is - // what's stored in the database even though it's not. Alice and Bob's computers - // will display different annotations for the same key, indefinitely. - // - // Note that we need a version number because the timestamp isn't guaranteed to - // be unique. So in the example above Alice and Bob might wind up updating with - // the same timestamp. - // - // You may also wonder why we use DateTime.Now instead of letting the database - // assign the timestamp itself. The reason is that the database might be running - // on a different machine than the puzzle server, and it might be using a different - // time zone. - - // First, detach the annotation from the context so the context doesn't think the annotation is in the database. + // same puzzle ID, team ID, and key. (This means there was a race condition: + // between the time we checked for the existence of a matching annotation and + // now, there was another insert.) So, we need to update the existing one. + + // But first, we need to detach the annotation from the context so the context + // doesn't think the annotation is in the database. + context.Entry(annotation).State = EntityState.Detached; - - try { - var sqlCommand = "UPDATE Annotations SET Version = Version + 1, Contents = @Contents, Timestamp = @Timestamp WHERE PuzzleID = @PuzzleID AND TeamID = @TeamID AND [Key] = @Key"; - int result = await context.Database.ExecuteSqlCommandAsync(sqlCommand, - new SqlParameter("@Contents", annotationRequest.contents), - new SqlParameter("@Timestamp", DateTime.Now), - new SqlParameter("@PuzzleID", puzzleId), - new SqlParameter("@TeamID", teamId), - new SqlParameter("@Key", annotationRequest.key)); - if (result != 1) { - response.AddError("Annotation update failed."); - } - } - catch (DbUpdateException) { - response.AddError("Encountered error while trying to update annotation."); - } - catch (Exception) { - response.AddError("Miscellaneous error while trying to update annotation."); - } + UpdateOneAnnotation(response, puzzleId, teamId, key, contents); } } } + /// + /// This routine stores in the database any annotations the requester has uploaded. + /// + private async Task StoreAnnotations(DecodedSyncRequest request, SyncResponse response, int puzzleId, int teamId) + { + if (request.AnnotationRequests == null) { + return; + } + + foreach (var annotationRequest in request.AnnotationRequests) { + await InsertOrUpdateOneAnnotation(response, puzzleId, teamId, annotationRequest.key, annotationRequest.contents); + } + } + /// /// This routine fetches the list of annotations that the requester's team has made since the last /// time the requester got a list of annotations. From 03a6cecfda5e0899e154be1e0e3aef8b2fb4815b Mon Sep 17 00:00:00 2001 From: Jay Lorch Date: Wed, 24 Apr 2019 20:37:23 -0700 Subject: [PATCH 3/3] Use FirstOrDefaultAsync instead of SingleOrDefaultAsync, per Morgan's suggestion --- ServerCore/SyncController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ServerCore/SyncController.cs b/ServerCore/SyncController.cs index 81c050d6..2a053517 100755 --- a/ServerCore/SyncController.cs +++ b/ServerCore/SyncController.cs @@ -354,7 +354,7 @@ private async Task InsertOrUpdateOneAnnotation(SyncResponse response, int puzzle // Check to see if the annotation already exists. If so, update it and we're done. Annotation existingAnnotation = - await context.Annotations.SingleOrDefaultAsync(a => a.PuzzleID == puzzleId && a.TeamID == teamId && a.Key == key); + await context.Annotations.FirstOrDefaultAsync(a => a.PuzzleID == puzzleId && a.TeamID == teamId && a.Key == key); if (existingAnnotation != null) { context.Entry(existingAnnotation).State = EntityState.Detached;