Skip to content
This repository has been archived by the owner on Jul 30, 2024. It is now read-only.
/ NuGet.Jobs Public archive

Commit

Permalink
Refactoring: moved FeedHelpers.GetCatalogPropertiesAsync (#414)
Browse files Browse the repository at this point in the history
  • Loading branch information
xavierdecoster authored Nov 27, 2018
1 parent a1a4bf4 commit 11d3d81
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 258 deletions.
68 changes: 68 additions & 0 deletions src/Catalog/Helpers/CatalogProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NuGet.Services.Metadata.Catalog.Persistence;

namespace NuGet.Services.Metadata.Catalog.Helpers
{
Expand All @@ -17,5 +22,68 @@ public CatalogProperties(DateTime? lastCreated, DateTime? lastDeleted, DateTime?
LastDeleted = lastDeleted;
LastEdited = lastEdited;
}

/// <summary>
/// Asynchronously reads and returns top-level <see cref="DateTime" /> metadata from the catalog's index.json.
/// </summary>
/// <remarks>The metadata values include "nuget:lastCreated", "nuget:lastDeleted", and "nuget:lastEdited",
/// which are the timestamps of the catalog cursor.</remarks>
/// <param name="storage"></param>
/// <param name="cancellationToken"></param>
/// <param name="telemetryService"></param>
/// <returns>A task that represents the asynchronous operation.
/// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="CatalogProperties" />.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="storage" /> is <c>null</c>.</exception>
/// <exception cref="OperationCanceledException">Thrown if <paramref name="cancellationToken" />
/// is cancelled.</exception>
public static async Task<CatalogProperties> ReadAsync(
IStorage storage,
ITelemetryService telemetryService,
CancellationToken cancellationToken)
{
if (storage == null)
{
throw new ArgumentNullException(nameof(storage));
}

if (telemetryService == null)
{
throw new ArgumentNullException(nameof(telemetryService));
}

cancellationToken.ThrowIfCancellationRequested();

DateTime? lastCreated = null;
DateTime? lastDeleted = null;
DateTime? lastEdited = null;

var stopwatch = Stopwatch.StartNew();
var indexUri = storage.ResolveUri("index.json");
var json = await storage.LoadStringAsync(indexUri, cancellationToken);

if (json != null)
{
var obj = JObject.Parse(json);
telemetryService.TrackCatalogIndexReadDuration(stopwatch.Elapsed, indexUri);
JToken token;

if (obj.TryGetValue("nuget:lastCreated", out token))
{
lastCreated = token.ToObject<DateTime>().ToUniversalTime();
}

if (obj.TryGetValue("nuget:lastDeleted", out token))
{
lastDeleted = token.ToObject<DateTime>().ToUniversalTime();
}

if (obj.TryGetValue("nuget:lastEdited", out token))
{
lastEdited = token.ToObject<DateTime>().ToUniversalTime();
}
}

return new CatalogProperties(lastCreated, lastDeleted, lastEdited);
}
}
}
65 changes: 0 additions & 65 deletions src/Catalog/Helpers/FeedHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NuGet.Services.Metadata.Catalog.Persistence;

namespace NuGet.Services.Metadata.Catalog.Helpers
Expand All @@ -30,69 +28,6 @@ public static HttpClient CreateHttpClient(Func<HttpMessageHandler> handlerFunc)
return new HttpClient(handler);
}

/// <summary>
/// Asynchronously reads and returns top-level <see cref="DateTime" /> metadata from the catalog's index.json.
/// </summary>
/// <remarks>The metadata values include "nuget:lastCreated", "nuget:lastDeleted", and "nuget:lastEdited",
/// which are the timestamps of the catalog cursor.</remarks>
/// <param name="storage"></param>
/// <param name="cancellationToken"></param>
/// <param name="telemetryService"></param>
/// <returns>A task that represents the asynchronous operation.
/// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="CatalogProperties" />.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="storage" /> is <c>null</c>.</exception>
/// <exception cref="OperationCanceledException">Thrown if <paramref name="cancellationToken" />
/// is cancelled.</exception>
public static async Task<CatalogProperties> GetCatalogPropertiesAsync(
IStorage storage,
ITelemetryService telemetryService,
CancellationToken cancellationToken)
{
if (storage == null)
{
throw new ArgumentNullException(nameof(storage));
}

if (telemetryService == null)
{
throw new ArgumentNullException(nameof(telemetryService));
}

cancellationToken.ThrowIfCancellationRequested();

DateTime? lastCreated = null;
DateTime? lastDeleted = null;
DateTime? lastEdited = null;

var stopwatch = Stopwatch.StartNew();
var indexUri = storage.ResolveUri("index.json");
var json = await storage.LoadStringAsync(indexUri, cancellationToken);

if (json != null)
{
var obj = JObject.Parse(json);
telemetryService.TrackCatalogIndexReadDuration(stopwatch.Elapsed, indexUri);
JToken token;

if (obj.TryGetValue("nuget:lastCreated", out token))
{
lastCreated = token.ToObject<DateTime>().ToUniversalTime();
}

if (obj.TryGetValue("nuget:lastDeleted", out token))
{
lastDeleted = token.ToObject<DateTime>().ToUniversalTime();
}

if (obj.TryGetValue("nuget:lastEdited", out token))
{
lastEdited = token.ToObject<DateTime>().ToUniversalTime();
}
}

return new CatalogProperties(lastCreated, lastDeleted, lastEdited);
}

/// <summary>
/// Builds a <see cref="Uri"/> for accessing the metadata of a specific package on the feed.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Ng/Jobs/Feed2CatalogJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ protected override async Task RunInternalAsync(CancellationToken cancellationTok
packagesEdited = 0;

// baseline timestamps
var catalogProperties = await FeedHelpers.GetCatalogPropertiesAsync(CatalogStorage, TelemetryService, cancellationToken);
var catalogProperties = await CatalogProperties.ReadAsync(CatalogStorage, TelemetryService, cancellationToken);
var lastCreated = catalogProperties.LastCreated ?? (StartDate ?? Constants.DateTimeMinValueUtc);
var lastEdited = catalogProperties.LastEdited ?? lastCreated;
var lastDeleted = catalogProperties.LastDeleted ?? lastCreated;
Expand Down
2 changes: 1 addition & 1 deletion src/Ng/Jobs/Package2CatalogJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ protected override async Task RunInternalAsync(CancellationToken cancellationTok
Logger.LogInformation($"Downloading {packages.Select(t => t.Value.Count).Sum()} packages");

// the idea here is to leave the lastCreated, lastEdited and lastDeleted values exactly as they were
var catalogProperties = await FeedHelpers.GetCatalogPropertiesAsync(_storage, TelemetryService, cancellationToken);
var catalogProperties = await CatalogProperties.ReadAsync(_storage, TelemetryService, cancellationToken);
var lastCreated = catalogProperties.LastCreated ?? DateTime.MinValue.ToUniversalTime();
var lastEdited = catalogProperties.LastEdited ?? DateTime.MinValue.ToUniversalTime();
var lastDeleted = catalogProperties.LastDeleted ?? DateTime.MinValue.ToUniversalTime();
Expand Down
198 changes: 198 additions & 0 deletions tests/CatalogTests/Helpers/CatalogPropertiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using NuGet.Services.Metadata.Catalog;
using NuGet.Services.Metadata.Catalog.Helpers;
using NuGet.Services.Metadata.Catalog.Persistence;
using Xunit;

namespace CatalogTests.Helpers
Expand Down Expand Up @@ -31,5 +36,198 @@ public void Constructor_InitializesPropertiesWithNonNullValues()
Assert.Equal(lastDeleted, properties.LastDeleted);
Assert.Equal(lastEdited, properties.LastEdited);
}

[Fact]
public async Task ReadAsync_ThrowsForNullStorage()
{
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => CatalogProperties.ReadAsync(
storage: null,
telemetryService: Mock.Of<ITelemetryService>(),
cancellationToken: CancellationToken.None));

Assert.Equal("storage", exception.ParamName);
}

[Fact]
public async Task ReadAsync_ThrowsForNullTelemetryService()
{
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => CatalogProperties.ReadAsync(
storage: Mock.Of<IStorage>(),
telemetryService: null,
cancellationToken: CancellationToken.None));

Assert.Equal("telemetryService", exception.ParamName);
}

[Fact]
public async Task ReadAsync_ThrowsIfCancelled()
{
await Assert.ThrowsAsync<OperationCanceledException>(
() => CatalogProperties.ReadAsync(
Mock.Of<IStorage>(),
Mock.Of<ITelemetryService>(),
new CancellationToken(canceled: true)));
}

[Fact]
public async Task ReadAsync_ReturnsNullPropertiesIfStorageReturnsNull()
{
var storage = CreateStorageMock(json: null);

var catalogProperties = await CatalogProperties.ReadAsync(
storage.Object,
Mock.Of<ITelemetryService>(),
CancellationToken.None);

Assert.NotNull(catalogProperties);
Assert.Null(catalogProperties.LastCreated);
Assert.Null(catalogProperties.LastDeleted);
Assert.Null(catalogProperties.LastEdited);

storage.Verify();
}

[Fact]
public async Task ReadAsync_ReturnsNullPropertiesIfPropertiesMissing()
{
var storage = CreateStorageMock(json: "{}");

var catalogProperties = await CatalogProperties.ReadAsync(
storage.Object,
Mock.Of<ITelemetryService>(),
CancellationToken.None);

Assert.NotNull(catalogProperties);
Assert.Null(catalogProperties.LastCreated);
Assert.Null(catalogProperties.LastDeleted);
Assert.Null(catalogProperties.LastEdited);

storage.Verify();
}

[Fact]
public async Task ReadAsync_ReturnsAllPropertiesIfAllPropertiesSet()
{
var lastCreatedDatetimeOffset = CreateDateTimeOffset(TimeSpan.FromMinutes(-5));
var lastDeletedDatetimeOffset = CreateDateTimeOffset(TimeSpan.Zero);
var lastEditedDatetimeOffset = CreateDateTimeOffset(TimeSpan.FromMinutes(-3));
var json = $"{{\"nuget:lastCreated\":\"{(lastCreatedDatetimeOffset.ToString("O"))}\"," +
$"\"nuget:lastDeleted\":\"{(lastDeletedDatetimeOffset.ToString("O"))}\"," +
$"\"nuget:lastEdited\":\"{(lastEditedDatetimeOffset.ToString("O"))}\"}}";
var storage = CreateStorageMock(json);

var catalogProperties = await CatalogProperties.ReadAsync(
storage.Object,
Mock.Of<ITelemetryService>(),
CancellationToken.None);

Assert.NotNull(catalogProperties);
Assert.NotNull(catalogProperties.LastCreated);
Assert.NotNull(catalogProperties.LastDeleted);
Assert.NotNull(catalogProperties.LastEdited);
Assert.Equal(lastCreatedDatetimeOffset, catalogProperties.LastCreated.Value);
Assert.Equal(lastDeletedDatetimeOffset, catalogProperties.LastDeleted.Value);
Assert.Equal(lastEditedDatetimeOffset, catalogProperties.LastEdited.Value);

storage.Verify();
}

[Fact]
public async Task ReadAsync_ReturnsDateTimeWithFractionalHourOffsetInUtc()
{
var lastCreated = CreateDateTimeOffset(new TimeSpan(hours: 5, minutes: 30, seconds: 0));
var json = CreateCatalogJson("nuget:lastCreated", lastCreated);

await VerifyPropertyAsync(json, catalogProperties =>
{
Assert.NotNull(catalogProperties.LastCreated);
Assert.Equal(lastCreated, catalogProperties.LastCreated.Value);
Assert.Equal(DateTimeKind.Utc, catalogProperties.LastCreated.Value.Kind);
});
}

[Fact]
public async Task ReadAsync_ReturnsDateTimeWithPositiveOffsetInUtc()
{
var lastDeleted = CreateDateTimeOffset(TimeSpan.FromHours(1));
var json = CreateCatalogJson("nuget:lastDeleted", lastDeleted);

await VerifyPropertyAsync(json, catalogProperties =>
{
Assert.NotNull(catalogProperties.LastDeleted);
Assert.Equal(lastDeleted, catalogProperties.LastDeleted.Value);
Assert.Equal(DateTimeKind.Utc, catalogProperties.LastDeleted.Value.Kind);
});
}

[Fact]
public async Task ReadAsync_ReturnsDateTimeWithNegativeOffsetInUtc()
{
var lastEdited = CreateDateTimeOffset(TimeSpan.FromHours(-1));
var json = CreateCatalogJson("nuget:lastEdited", lastEdited);

await VerifyPropertyAsync(json, catalogProperties =>
{
Assert.NotNull(catalogProperties.LastEdited);
Assert.Equal(lastEdited, catalogProperties.LastEdited.Value);
Assert.Equal(DateTimeKind.Utc, catalogProperties.LastEdited.Value.Kind);
});
}

[Fact]
public async Task ReadAsync_ReturnUtcDateTimeInUtc()
{
var lastCreated = DateTimeOffset.UtcNow;
var json = CreateCatalogJson("nuget:lastCreated", lastCreated);

await VerifyPropertyAsync(json, catalogProperties =>
{
Assert.NotNull(catalogProperties.LastCreated);
Assert.Equal(lastCreated, catalogProperties.LastCreated.Value);
Assert.Equal(DateTimeKind.Utc, catalogProperties.LastCreated.Value.Kind);
});
}

private static string CreateCatalogJson(string propertyName, DateTimeOffset propertyValue)
{
return $"{{\"{propertyName}\":\"{(propertyValue.ToString("O"))}\"}}";
}

private static DateTimeOffset CreateDateTimeOffset(TimeSpan offset)
{
var datetime = new DateTime(DateTime.Now.Ticks, DateTimeKind.Unspecified);

return new DateTimeOffset(datetime, offset);
}

private static async Task VerifyPropertyAsync(string json, Action<CatalogProperties> propertyVerifier)
{
var storage = CreateStorageMock(json);

var catalogProperties = await CatalogProperties.ReadAsync(
storage.Object,
Mock.Of<ITelemetryService>(),
CancellationToken.None);

Assert.NotNull(catalogProperties);
propertyVerifier(catalogProperties);
storage.Verify();
}

private static Mock<IStorage> CreateStorageMock(string json)
{
var storage = new Mock<IStorage>(MockBehavior.Strict);

storage.Setup(x => x.ResolveUri(It.IsNotNull<string>()))
.Returns(new Uri("https://unit.test"))
.Verifiable();
storage.Setup(x => x.LoadStringAsync(It.IsNotNull<Uri>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(json)
.Verifiable();

return storage;
}
}
}
Loading

0 comments on commit 11d3d81

Please sign in to comment.