Skip to content

Commit

Permalink
Merge Redis integration (#112)
Browse files Browse the repository at this point in the history
* Feature/use distributed caching (#110)

* Implement Redis caching

* Reduce tweet attempts to just 3 (#111)
  • Loading branch information
Zifah authored Sep 16, 2024
1 parent d05328a commit ea50305
Show file tree
Hide file tree
Showing 18 changed files with 245 additions and 111 deletions.
1 change: 1 addition & 0 deletions Api/Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="4.6.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
Expand Down
6 changes: 3 additions & 3 deletions Api/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public async Task<IActionResult> Search([FromQuery(Name = "q"), Required] string
var matches = await _searchService.Search(searchTerm);

// TODO: Check if the comparison here removes takes diacrits into consideration
if (matches.Count() == 1 && matches.First().Name.ToLower() == searchTerm.ToLower())
if (matches.Count() == 1 && matches.First().Name.Equals(searchTerm, StringComparison.CurrentCultureIgnoreCase))
{
await _eventPubService.PublishEvent(new ExactNameSearched(searchTerm));
await _eventPubService.PublishEvent(new ExactNameSearched(matches.First().Name));
}

return Ok(matches.MapToDtoCollection());
Expand Down Expand Up @@ -96,7 +96,7 @@ public async Task<IActionResult> SearchOne(string searchTerm)

if(nameEntry != null)
{
await _eventPubService.PublishEvent(new ExactNameSearched(searchTerm));
await _eventPubService.PublishEvent(new ExactNameSearched(nameEntry.Name));
}

return Ok(nameEntry);
Expand Down
31 changes: 23 additions & 8 deletions Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
using System.Text.Json.Serialization;
using Hangfire;
using Infrastructure.Hangfire;
using Infrastructure.Redis;
using Ardalis.GuardClauses;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
Expand All @@ -44,8 +46,15 @@
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole(Role.ADMIN.ToString()));
options.AddPolicy("AdminAndLexicographers", policy => policy.RequireRole(Role.ADMIN.ToString(), Role.PRO_LEXICOGRAPHER.ToString(), Role.BASIC_LEXICOGRAPHER.ToString()));
options.AddPolicy("AdminAndProLexicographers", policy => policy.RequireRole(Role.ADMIN.ToString(), Role.PRO_LEXICOGRAPHER.ToString()));
options.AddPolicy("AdminAndLexicographers", policy => policy.RequireRole(
Role.ADMIN.ToString(),
Role.PRO_LEXICOGRAPHER.ToString(),
Role.BASIC_LEXICOGRAPHER.ToString()
));
options.AddPolicy("AdminAndProLexicographers", policy => policy.RequireRole(
Role.ADMIN.ToString(),
Role.PRO_LEXICOGRAPHER.ToString()
));
});

services.AddControllers().AddJsonOptions(options =>
Expand Down Expand Up @@ -87,10 +96,12 @@
});
});
var mongoDbSettings = configuration.GetRequiredSection("MongoDB");
services.InitializeDatabase(mongoDbSettings.GetValue<string>("ConnectionString"), mongoDbSettings.GetValue<string>("DatabaseName"));
services.InitializeDatabase(
Guard.Against.NullOrEmpty(mongoDbSettings.GetValue<string>("ConnectionString")),
Guard.Against.NullOrEmpty(mongoDbSettings.GetValue<string>("DatabaseName")));

builder.Services.AddTransient(x =>
new MySqlConnection(builder.Configuration.GetSection("MySQL:ConnectionString").Value));
new MySqlConnection(Guard.Against.NullOrEmpty(configuration.GetSection("MySQL:ConnectionString").Value)));

services.AddSingleton<NameEntryService>();
services.AddSingleton<GeoLocationsService>();
Expand All @@ -103,8 +114,8 @@
services.AddScoped<EmbeddedVideoValidator>();
services.AddScoped<EtymologyValidator>();
services.AddScoped<SqlToMongoMigrator>();
services.AddSingleton<IRecentIndexesCache, RecentIndexesCache>();
services.AddSingleton<IRecentSearchesCache, RecentSearchesCache>();
services.AddSingleton<IRecentIndexesCache, RedisRecentIndexesCache>();
services.AddSingleton<IRecentSearchesCache, RedisRecentSearchesCache>();

//Validation
services.AddValidatorsFromAssemblyContaining<CreateUserValidator>();
Expand All @@ -116,7 +127,8 @@
services.AddTwitterClient(configuration);

builder.Services.AddMemoryCache();
builder.Services.SetupHangfire(configuration.GetRequiredSection("MongoDB:ConnectionString").Value!);
builder.Services.SetupHangfire(Guard.Against.NullOrEmpty(configuration.GetRequiredSection("MongoDB:ConnectionString").Value));
builder.Services.SetupRedis(configuration);


var app = builder.Build();
Expand All @@ -137,6 +149,9 @@

app.MapControllers();

app.UseHangfireDashboard("/backJobMonitor");
if (app.Environment.IsDevelopment())
{
app.UseHangfireDashboard("/backJobMonitor");
}

app.Run();
3 changes: 3 additions & 0 deletions Api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@
"Twitter": {
"TweetTemplate": "{name}: \"{meaning}\" {link}",
"TweetIntervalSeconds": 60
},
"Redis": {
"DatabaseIndex": 1
}
}
6 changes: 6 additions & 0 deletions Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Redis": "redis:6379"
},
"Twitter": {
"AccessToken": "your-access-token",
"AccessTokenSecret": "your-access-token-secret",
Expand All @@ -15,5 +18,8 @@
"NameUrlPrefix": "https://www.yorubaname.com/entries",
"TweetTemplate": "New name entry: {name}, {meaning}. More here: {link}",
"TweetIntervalSeconds": 180
},
"Redis": {
"DatabaseIndex": 0
}
}
39 changes: 39 additions & 0 deletions Application/Cache/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Application.Cache
{
public interface ICacheService<T>
{
/// <summary>
/// Retrieves a collection of items from the cache.
/// </summary>
/// <param name="key">The key under which the items are cached.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of items.</returns>
Task<IEnumerable<T>> GetAsync(string key);

/// <summary>
/// Adds an item to the cache and updates its recency.
/// </summary>
/// <param name="key">The key under which the item is cached.</param>
/// <param name="item">The item to be added to the cache.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task StackAsync(string key, T item);

/// <summary>
/// Removes an item from the cache.
/// </summary>
/// <param name="key">The key of the item to be removed.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the removal was successful.</returns>
Task RemoveAsync(string key, string searchTerm);

/// <summary>
/// Retrieves the most popular items based on their frequency.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of the most popular items.</returns>
Task<IEnumerable<string>> GetMostPopularAsync(string key);
}
}
63 changes: 0 additions & 63 deletions Application/Cache/InMemoryCache.cs

This file was deleted.

8 changes: 0 additions & 8 deletions Application/Cache/RecentIndexesCache.cs

This file was deleted.

28 changes: 0 additions & 28 deletions Application/Cache/RecentSearchesCache.cs

This file was deleted.

7 changes: 7 additions & 0 deletions Infrastructure/Configuration/RedisConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Infrastructure.Configuration
{
public record RedisConfig
{
public int DatabaseIndex { get; set; }
}
}
3 changes: 2 additions & 1 deletion Infrastructure/Hangfire/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Api.Utilities;
using Ardalis.GuardClauses;

namespace Infrastructure.Hangfire
{
Expand All @@ -20,7 +21,7 @@ public static IServiceCollection SetupHangfire(this IServiceCollection services,
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMongoStorage(mongoClient, mongoUrlBuilder.DatabaseName, new MongoStorageOptions
.UseMongoStorage(mongoClient, Guard.Against.NullOrEmpty(mongoUrlBuilder.DatabaseName), new MongoStorageOptions
{
MigrationOptions = new MongoMigrationOptions
{
Expand Down
2 changes: 2 additions & 0 deletions Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="4.6.0" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.14" />
<PackageReference Include="Hangfire.Mongo" Version="1.10.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
<PackageReference Include="TweetinviAPI" Version="5.0.4" />
</ItemGroup>

Expand Down
21 changes: 21 additions & 0 deletions Infrastructure/Redis/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Ardalis.GuardClauses;
using Infrastructure.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

namespace Infrastructure.Redis
{
public static class DependencyInjection
{
private const string SectionName = "Redis";

public static IServiceCollection SetupRedis(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<RedisConfig>(configuration.GetRequiredSection(SectionName));
var redisConnectionString = Guard.Against.NullOrEmpty(configuration.GetConnectionString(SectionName));
services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(redisConnectionString));
return services;
}
}
}
13 changes: 13 additions & 0 deletions Infrastructure/Redis/RedisCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Infrastructure.Configuration;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

namespace Infrastructure.Redis
{
public abstract class RedisCache(
IConnectionMultiplexer connectionMultiplexer,
IOptions<RedisConfig> redisConfig)
{
protected readonly IDatabase _cache = connectionMultiplexer.GetDatabase(redisConfig.Value.DatabaseIndex);
}
}
46 changes: 46 additions & 0 deletions Infrastructure/Redis/RedisRecentIndexesCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Core.Cache;
using Infrastructure.Configuration;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

namespace Infrastructure.Redis
{
public class RedisRecentIndexesCache(
IConnectionMultiplexer connectionMultiplexer,
IOptions<RedisConfig> redisConfig) : RedisCache(connectionMultiplexer, redisConfig), IRecentIndexesCache
{
private const string Key = "recent_indexes";
private const int MaxItemsToReturn = 5;
private const int MaxItemsToStore = 10;

public async Task<IEnumerable<string>> Get()
{
var results = await _cache.SortedSetRangeByRankAsync(Key, 0, MaxItemsToReturn - 1, Order.Descending);
return results.Select(r => r.ToString());
}

public async Task<bool> Remove(string item)
{
var tran = _cache.CreateTransaction();
_ = tran.SortedSetRemoveAsync(Key, item);
return await tran.ExecuteAsync();
}

public async Task Stack(string item)
{
// Use a Redis transaction to ensure atomicity of both operations
var transaction = _cache.CreateTransaction();

// Add the search term to the front of the Redis list
_ = transaction.SortedSetAddAsync(Key, item, DateTime.UtcNow.Ticks);
_ = transaction.SortedSetRemoveRangeByRankAsync(Key, 0, -(MaxItemsToStore + 1));

// Execute the transaction
bool committed = await transaction.ExecuteAsync();
if (!committed)
{
throw new Exception("Redis Transaction failed");
}
}
}
}
Loading

0 comments on commit ea50305

Please sign in to comment.