private void ProcessPackageEdits(int packageKey) { // Create a fresh entities context so that we work in isolation var entitiesContext = new EntitiesContext(ConnectionString.ConnectionString, readOnly: false); // Get the list of edits for this package // Do edit with a 'most recent edit to this package wins - other edits are deleted' strategy. var editsForThisPackage = entitiesContext.Set <PackageEdit>() .Where(pe => pe.PackageKey == packageKey && pe.TriedCount < 3) .Include(pe => pe.Package) .Include(pe => pe.Package.PackageRegistration) .Include(pe => pe.User) .OrderByDescending(pe => pe.Timestamp) .ToList(); // List of Work to do: // 1) Backup old blob, if the original has not been backed up yet // 2) Downloads blob, create new NUPKG locally // 3) Upload blob // 4) Update the database PackageEdit edit = editsForThisPackage.First(); var blobClient = StorageAccount.CreateCloudBlobClient(); var packagesContainer = Util.GetPackagesBlobContainer(blobClient); var latestPackageFileName = Util.GetPackageFileName(edit.Package.PackageRegistration.Id, edit.Package.Version); var originalPackageFileName = Util.GetBackupOfOriginalPackageFileName(edit.Package.PackageRegistration.Id, edit.Package.Version); var originalPackageBackupBlob = packagesContainer.GetBlockBlobReference(originalPackageFileName); var latestPackageBlob = packagesContainer.GetBlockBlobReference(latestPackageFileName); var edits = new List <Action <ManifestMetadata> > { (m) => { m.Authors = edit.Authors; }, (m) => { m.Copyright = edit.Copyright; }, (m) => { m.Description = edit.Description; }, (m) => { m.IconUrl = edit.IconUrl; }, (m) => { m.LicenseUrl = edit.LicenseUrl; }, (m) => { m.ProjectUrl = edit.ProjectUrl; }, (m) => { m.ReleaseNotes = edit.ReleaseNotes; }, (m) => { m.RequireLicenseAcceptance = edit.RequiresLicenseAcceptance; }, (m) => { m.Summary = edit.Summary; }, (m) => { m.Title = edit.Title; }, (m) => { m.Tags = edit.Tags; }, }; Log.Info( "Processing Edit Key={0}, PackageId={1}, Version={2}, User={3}", edit.Key, edit.Package.PackageRegistration.Id, edit.Package.Version, edit.User.Username); if (!WhatIf) { edit.TriedCount += 1; int nr = entitiesContext.SaveChanges(); if (nr != 1) { throw new Exception( String.Format("Something went terribly wrong, only one entity should be updated but actually {0} entities were updated", nr)); } } try { ArchiveOriginalPackageBlob(originalPackageBackupBlob, latestPackageBlob); using (var readWriteStream = new MemoryStream()) { // Download to memory CloudBlockBlob downloadSourceBlob = WhatIf ? latestPackageBlob : originalPackageBackupBlob; Log.Info("Downloading original package blob to memory {0}", downloadSourceBlob.Name); downloadSourceBlob.DownloadToStream(readWriteStream); // Rewrite in memory Log.Info("Rewriting nupkg package in memory", downloadSourceBlob.Name); NupkgRewriter.RewriteNupkgManifest(readWriteStream, edits); // Get updated hash code, and file size Log.Info("Computing updated hash code of memory stream"); var newPackageFileSize = readWriteStream.Length; var hashAlgorithm = HashAlgorithm.Create("SHA512"); byte[] hashBytes = hashAlgorithm.ComputeHash(readWriteStream.GetBuffer()); var newHash = Convert.ToBase64String(hashBytes); if (!WhatIf) { // Snapshot the blob var blobSnapshot = latestPackageBlob.CreateSnapshot(); // Build up the changes in the entities context edit.Apply(hashAlgorithm: "SHA512", hash: newHash, packageFileSize: newPackageFileSize); foreach (var eachEdit in editsForThisPackage) { entitiesContext.DeleteOnCommit(eachEdit); } // Upload the blob before doing SaveChanges(). If blob update fails, we won't do SaveChanges() and the edit can be retried. // If SaveChanges() fails we can undo the blob upload. try { Log.Info("Uploading blob from memory {0}", latestPackageBlob.Name); readWriteStream.Position = 0; latestPackageBlob.UploadFromStream(readWriteStream); } catch (Exception e) { Log.Error("(error) - package edit blob update failed."); Log.ErrorException("(exception)", e); Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); throw; // To handler block that will record error in DB } try { // SaveChanges tries to commit changes to DB entitiesContext.SaveChanges(); } catch (Exception e) { // Commit changes to DB probably failed. // Since our blob update wasn't part of the transaction (and doesn't AFAIK have a 'commit()' operator we can utilize for the type of blobs we are using) // try, (single attempt) to roll back the blob update by restoring the previous snapshot. Log.Error("(error) - package edit DB update failed. Trying to roll back the blob to its previous snapshot."); Log.ErrorException("(exception)", e); Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); try { latestPackageBlob.StartCopyFromBlob(blobSnapshot); } catch (Exception e2) { // If blob rollback fails it is not be the end of the world // - the package metadata mismatches the edit now, // but there should still an edit in the queue, waiting to be rerun and put everything back in synch. Log.Error("(error) - rolling back the package blob to its previous snapshot failed."); Log.ErrorException("(exception)", e2); Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); } throw; // To handler block that will record error in DB } } } } catch (Exception e) { if (!WhatIf) { try { Log.Info("Storing the error on package edit with key {0}", edit.Key); // Try to record the error into the PackageEdit database record // so that we can actually diagnose failures. // This must be done on a fresh context to ensure no conflicts. var errorContext = new EntitiesContext(ConnectionString.ConnectionString, readOnly: false); var errorEdit = errorContext.Set <PackageEdit>().Where(pe => pe.Key == edit.Key).FirstOrDefault(); if (errorEdit != null) { errorEdit.LastError = string.Format("{0} : {1}", e.GetType(), e); errorContext.SaveChanges(); } else { Log.Info("The package edit with key {0} couldn't be found. It was likely canceled and deleted.", edit.Key); } } catch (Exception errorException) { Log.ErrorException("(error) - couldn't save the last error on the edit that was being applied.", errorException); } } } }
private void ProcessPackageEdits(IEnumerable <PackageEdit> editsForThisPackage, EntitiesContext entitiesContext) { // List of Work to do: // 1) Backup old blob, if the original has not been backed up yet // 2) Downloads blob, create new NUPKG locally // 3) Upload blob // 4) Update the database PackageEdit edit = editsForThisPackage.OrderByDescending(pe => pe.Timestamp).First(); var blobClient = StorageAccount.CreateCloudBlobClient(); var packagesContainer = Util.GetPackagesBlobContainer(blobClient); var latestPackageFileName = Util.GetPackageFileName(edit.Package.PackageRegistration.Id, edit.Package.Version); var originalPackageFileName = Util.GetBackupOfOriginalPackageFileName(edit.Package.PackageRegistration.Id, edit.Package.Version); var originalPackageBackupBlob = packagesContainer.GetBlockBlobReference(originalPackageFileName); var latestPackageBlob = packagesContainer.GetBlockBlobReference(latestPackageFileName); var edits = new List <Action <ManifestMetadata> > { (m) => { m.Authors = edit.Authors; }, (m) => { m.Copyright = edit.Copyright; }, (m) => { m.Description = edit.Description; }, (m) => { m.IconUrl = edit.IconUrl; }, (m) => { m.LicenseUrl = edit.LicenseUrl; }, (m) => { m.ProjectUrl = edit.ProjectUrl; }, (m) => { m.ReleaseNotes = edit.ReleaseNotes; }, (m) => { m.RequireLicenseAcceptance = edit.RequiresLicenseAcceptance; }, (m) => { m.Summary = edit.Summary; }, (m) => { m.Title = edit.Title; }, (m) => { m.Tags = edit.Tags; }, }; Log.Info( "Processing Edit Key={0}, PackageId={1}, Version={2}", edit.Key, edit.Package.PackageRegistration.Id, edit.Package.Version); if (!WhatIf) { edit.TriedCount += 1; int nr = entitiesContext.SaveChanges(); if (nr != 1) { throw new ApplicationException( String.Format("Something went terribly wrong, only one entity should be updated but actually {0} entities were updated", nr)); } } ArchiveOriginalPackageBlob(originalPackageBackupBlob, latestPackageBlob); using (var readWriteStream = new MemoryStream()) { // Download to memory CloudBlockBlob downloadSourceBlob = WhatIf ? latestPackageBlob : originalPackageBackupBlob; Log.Info("Downloading original package blob to memory {0}", downloadSourceBlob.Name); downloadSourceBlob.DownloadToStream(readWriteStream); // Rewrite in memory Log.Info("Rewriting nupkg package in memory", downloadSourceBlob.Name); NupkgRewriter.RewriteNupkgManifest(readWriteStream, edits); // Get updated hash code, and file size Log.Info("Computing updated hash code of memory stream"); var newPackageFileSize = readWriteStream.Length; var hashAlgorithm = HashAlgorithm.Create("SHA512"); byte[] hashBytes = hashAlgorithm.ComputeHash(readWriteStream.GetBuffer()); var newHash = Convert.ToBase64String(hashBytes); if (!WhatIf) { // Snapshot the blob var blobSnapshot = latestPackageBlob.CreateSnapshot(); // Start Transaction: Complete the edit in the gallery DB. // Use explicit SQL transactions instead of EF operation-grouping // so that we can manually roll the transaction back on a blob related failure. ObjectContext objectContext = (entitiesContext as IObjectContextAdapter).ObjectContext; ((objectContext.Connection) as EntityConnection).Open(); // must open in order to begin transaction using (EntityTransaction transaction = ((objectContext.Connection) as EntityConnection).BeginTransaction()) { edit.Apply(hashAlgorithm: "SHA512", hash: newHash, packageFileSize: newPackageFileSize); // Add to transaction: delete all the pending edits of this package. foreach (var eachEdit in editsForThisPackage) { entitiesContext.DeleteOnCommit(eachEdit); } entitiesContext.SaveChanges(); // (transaction is still not committed, but do some EF legwork up-front of modifying the blob) try { // Reupload blob Log.Info("Uploading blob from memory {0}", latestPackageBlob.Name); readWriteStream.Position = 0; latestPackageBlob.UploadFromStream(readWriteStream); } catch (Exception e) { // Uploading the updated nupkg failed. // Rollback the transaction, which restores the Edit to PackageEdits so it can be attempted again. Log.Error("(error) - package edit blob update failed. Rolling back the DB transaction."); Log.ErrorException("(exception", e); Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); transaction.Rollback(); return; } try { transaction.Commit(); } catch (Exception e) { // Commit changes to DB failed. // Since our blob update wasn't part of the transaction (and doesn't AFAIK have a 'commit()' operator we can utilize for the type of blobs we are using) // try, (single attempt) to roll back the blob update by restoring the previous snapshot. Log.Error("(error) - package edit DB update failed. Trying to roll back the blob to its previous snapshot."); Log.ErrorException("(exception", e); Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); try { latestPackageBlob.StartCopyFromBlob(blobSnapshot); } catch (Exception e2) { // In this case it may not be the end of the world - the package metadata mismatches the edit now, // but there's still an edit in the queue, waiting to be rerun and put everything back in synch. Log.Error("(error) - rolling back the package blob to its previous snapshot failed."); Log.ErrorException("(exception", e2); Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); } } } } } }
private async Task ApplyEdit(PackageEdit edit) { string originalPath = null; try { TempDirectory = Path.Combine(Path.GetTempPath(), "NuGetService", "HandlePackageEdits"); var directory = Path.Combine(TempDirectory, edit.Id, edit.Version); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } originalPath = Path.Combine(directory, "original.nupkg"); var sourceItem = FileStorage.GetFile(edit.Id, edit.Version); Trace.TraceInformation($"Name is {sourceItem.Name}, storage uri is {sourceItem.Uri}"); // Download the original file Trace.TraceInformation($"Downloading original copy of {edit.Id} {edit.Version}"); await FileStorage.DownloadAsync(sourceItem, originalPath); Trace.TraceInformation($"Downloaded original copy of {edit.Id} {edit.Version}"); // Check that a backup exists Trace.TraceInformation($"Backing up original copy of {edit.Id} {edit.Version}"); await FileStorage.BackupAsync(originalPath, edit.Id, edit.Version, edit.Hash); Trace.TraceInformation($"Backed up original copy of {edit.Id} {edit.Version}"); // Update the nupkg manifest with the new metadata using (var originalStream = File.Open(originalPath, FileMode.Open, FileAccess.ReadWrite)) { Trace.TraceInformation($"Rewriting package file for {edit.Id} {edit.Version}"); var editActionList = edit.GetEditsAsActionList(); NupkgRewriter.RewriteNupkgManifest(originalStream, editActionList); Trace.TraceInformation($"Rewrote package file for {edit.Id} {edit.Version}"); } // Snapshot the original blob Trace.TraceInformation($"Snapshotting original blob for {edit.Id} {edit.Version} ({sourceItem.Uri.AbsoluteUri})."); var snapshot = await FileStorage.SnapshotService.CreateSnapshot(sourceItem); Trace.TraceInformation($"Snapshotted original blob for {edit.Id} {edit.Version} ({sourceItem.Uri.AbsoluteUri})."); // Upload the updated file Trace.TraceInformation($"Uploading modified package file for {edit.Id} {edit.Version} to {sourceItem.Uri.AbsoluteUri}"); await FileStorage.UploadAsync(sourceItem, originalPath); Trace.TraceInformation($"Uploaded modified package file for {edit.Id} {edit.Version} to {sourceItem.Uri.AbsoluteUri}"); // Calculate new size and hash string hash; long size; using (var originalStream = File.OpenRead(originalPath)) { size = originalStream.Length; var hashAlgorithm = HashAlgorithm.Create(HashAlgorithmName); hash = Convert.ToBase64String( hashAlgorithm.ComputeHash(originalStream)); } // Update the database try { Trace.TraceInformation($"Updating package record for {edit.Id} {edit.Version}"); await UpdateDatabaseWithEdit(edit, hash, size); Trace.TraceInformation($"Updated package record for {edit.Id} {edit.Version}"); } catch (Exception exception) { // Error occurred while updaing database, roll back the blob to the snapshot // Can't do "await" in a catch block, but this should be pretty quick since it just starts the copy Trace.TraceError($"Failed to update database! {exception}"); Trace.TraceWarning( $"Rolling back updated blob for {edit.Id} {edit.Version}. Copying snapshot {snapshot.Uri.AbsoluteUri} to {sourceItem.Uri.AbsoluteUri}"); FileStorage.SnapshotService.RestoreSnapshot(sourceItem, snapshot); Trace.TraceWarning( $"Rolled back updated blob for {edit.Id} {edit.Version}. Copying snapshot {snapshot.Uri.AbsoluteUri} to {sourceItem.Uri.AbsoluteUri}"); throw; } Trace.TraceInformation("Deleting snapshot blob {2} for {0} {1}.", edit.Id, edit.Version, snapshot.Uri.AbsoluteUri); await FileStorage.SnapshotService.DeleteSnapshotAsync(snapshot); Trace.TraceInformation("Deleted snapshot blob {2} for {0} {1}.", edit.Id, edit.Version, snapshot.Uri.AbsoluteUri); } finally { if (!string.IsNullOrEmpty(originalPath) && File.Exists(originalPath)) { File.Delete(originalPath); } } }
private async Task ApplyEdit(PackageEdit edit) { string originalPath = null; string originalReadMeMDPath = null; try { TempDirectory = Path.Combine(Path.GetTempPath(), "NuGetService", "HandlePackageEdits"); var directory = Path.Combine(TempDirectory, edit.Id, edit.Version); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } originalPath = Path.Combine(directory, "original.nupkg"); Logger.LogInformation("Downloading nupkg to {OriginalPath}", originalPath); var sourceBlob = SourceContainer.GetBlockBlobReference( StorageHelpers.GetPackageBlobName(edit.Id, edit.Version)); Logger.LogInformation("Name is {SourceBlobName}, storage uri is {StorageUri}", sourceBlob.Name, sourceBlob.StorageUri); // Download the original file Logger.LogInformation("Downloading original copy of {EditId} {EditVersion}", edit.Id, edit.Version); await sourceBlob.DownloadToFileAsync(originalPath, FileMode.Create); Logger.LogInformation("Downloaded original copy of {EditId} {EditVersion}", edit.Id, edit.Version); // Check that a backup exists var backupBlob = BackupsContainer.GetBlockBlobReference( StorageHelpers.GetPackageBackupBlobName(edit.Id, edit.Version, edit.Hash)); if (!await backupBlob.ExistsAsync()) { Logger.LogInformation("Backing up original copy of {EditId} {EditVersion}", edit.Id, edit.Version); await backupBlob.UploadFromFileAsync(originalPath); Logger.LogInformation("Backed up original copy of {EditId} {EditVersion}", edit.Id, edit.Version); } // Update the nupkg manifest with the new metadata using (var originalStream = File.Open(originalPath, FileMode.Open, FileAccess.ReadWrite)) { Logger.LogInformation("Rewriting package file for {EditId} {EditVersion}", edit.Id, edit.Version); var editActionList = edit.GetEditsAsActionList(); NupkgRewriter.RewriteNupkgManifest(originalStream, editActionList); Logger.LogInformation("Rewrote package file for {EditId} {EditVersion}", edit.Id, edit.Version); } // Snapshot the original blob Logger.LogInformation("Snapshotting original blob for {EditId} {EditVersion} ({BlobUri}).", edit.Id, edit.Version, sourceBlob.Uri.AbsoluteUri); var sourceSnapshot = await sourceBlob.CreateSnapshotAsync(); Logger.LogInformation("Snapshotted original blob for {EditId} {EditVersion} ({BlobUri}).", edit.Id, edit.Version, sourceBlob.Uri.AbsoluteUri); // Upload the updated file Logger.LogInformation("Uploading modified package file for {EditId} {EditVersion} to {BlobUri}", edit.Id, edit.Version, sourceBlob.Uri.AbsoluteUri); await sourceBlob.UploadFromFileAsync(originalPath); Logger.LogInformation("Uploaded modified package file for {EditId} {EditVersion} to {BlobUri}", edit.Id, edit.Version, sourceBlob.Uri.AbsoluteUri); // Calculate new size and hash string hash; long size; using (var originalStream = File.OpenRead(originalPath)) { size = originalStream.Length; var hashAlgorithm = HashAlgorithm.Create(HashAlgorithmName); hash = Convert.ToBase64String( hashAlgorithm.ComputeHash(originalStream)); } // ReadMe update var readMeMDBlob = await UpdateReadMeAsync(edit, directory, originalReadMeMDPath, MarkdownExtension); try { Logger.LogInformation("Updating package record for {EditId} {EditVersion}", edit.Id, edit.Version); await UpdateDatabaseWithEdit(edit, hash, size); Logger.LogInformation("Updated package record for {EditId} {EditVersion}", edit.Id, edit.Version); } catch (Exception exception) { // Error occurred while updaing database, roll back the blob to the snapshot Logger.LogError("Failed to update database! {Exception}", exception); Logger.LogWarning( "Rolling back updated blob for {EditId} {EditVersion}. Copying snapshot {SnapshotUri} to {BlobUri}", edit.Id, edit.Version, sourceSnapshot.Uri.AbsoluteUri, sourceBlob.Uri.AbsoluteUri); await sourceBlob.StartCopyAsync(sourceSnapshot); while (sourceBlob.CopyState.Status != CopyStatus.Success) { await Task.Delay(1000); } Logger.LogWarning( "Rolled back updated blob for {EditId} {EditVersion}. Copying snapshot {SnapshotUri} to {BlobUri}", sourceSnapshot.Uri.AbsoluteUri, sourceBlob.Uri.AbsoluteUri); await RollBackReadMeAsync(edit, directory, originalReadMeMDPath, readMeMDBlob.activeSnapshot, readMeMDBlob.activeBlob); throw; } if (edit.ReadMeState == ReadMeChanged) { // Delete pending ReadMes Logger.LogInformation("Deleting pending ReadMe for {EditId} {EditVersion} from {PendingReadMeMdUri}", edit.Id, edit.Version, readMeMDBlob.pendingBlob.Uri.AbsoluteUri); await readMeMDBlob.pendingBlob.DeleteIfExistsAsync(); Logger.LogInformation($"Deleted pending ReadMe for {edit.Id} {edit.Version} from {readMeMDBlob.pendingBlob.Uri.AbsoluteUri}"); } Logger.LogInformation("Deleting snapshot blob {SnapshotUri} for {EditId} {EditVersion}.", sourceSnapshot.Uri.AbsoluteUri, edit.Id, edit.Version); await sourceSnapshot.DeleteAsync(); Logger.LogInformation("Deleted snapshot blob {SnapshotUri} for {EditId} {EditVersion}.", sourceSnapshot.Uri.AbsoluteUri, edit.Id, edit.Version); if (readMeMDBlob.activeSnapshot != null) { Logger.LogInformation("Deleting snapshot ReadMe Md {ActiveReadMeMdUri} for {EditId} {EditVersion}.", readMeMDBlob.activeSnapshot.Uri.AbsoluteUri, edit.Id, edit.Version); await readMeMDBlob.activeSnapshot.DeleteAsync(); Logger.LogInformation("Deleted snapshot ReadMe Md {ActiveReadMeMdUri} for {EditId} {EditVersion}.", readMeMDBlob.activeSnapshot.Uri.AbsoluteUri, edit.Id, edit.Version); } } finally { if (!string.IsNullOrEmpty(originalPath) && File.Exists(originalPath)) { File.Delete(originalPath); } } }