|  | 
| 15 | 15 | using StackExchange.Redis; | 
| 16 | 16 | using Xunit; | 
| 17 | 17 | using Xunit.Abstractions; | 
|  | 18 | +using Aspire.Hosting.Tests.Dcp; | 
| 18 | 19 | 
 | 
| 19 | 20 | namespace Aspire.Hosting.Redis.Tests; | 
| 20 | 21 | 
 | 
| @@ -120,6 +121,111 @@ public async Task VerifyRedisResource() | 
| 120 | 121 |         Assert.Equal("value", value); | 
| 121 | 122 |     } | 
| 122 | 123 | 
 | 
|  | 124 | +    [Fact] | 
|  | 125 | +    [RequiresDocker] | 
|  | 126 | +    public async Task VerifyDatabasesAreNotDuplicatedForPersistentRedisInsightContainer() | 
|  | 127 | +    { | 
|  | 128 | +        var randomResourceSuffix = Random.Shared.Next(10000).ToString(); | 
|  | 129 | +        var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); | 
|  | 130 | + | 
|  | 131 | +        var configure = (DistributedApplicationOptions options) => | 
|  | 132 | +        { | 
|  | 133 | +            options.ContainerRegistryOverride = TestConstants.AspireTestContainerRegistry; | 
|  | 134 | +        }; | 
|  | 135 | + | 
|  | 136 | +        using var builder1 = TestDistributedApplicationBuilder.Create(configure, testOutputHelper); | 
|  | 137 | +        builder1.Configuration[$"DcpPublisher:ResourceNameSuffix"] = randomResourceSuffix; | 
|  | 138 | + | 
|  | 139 | +        IResourceBuilder<RedisInsightResource>? redisInsightBuilder = null; | 
|  | 140 | +        var redis1 = builder1.AddRedis("redisForInsightPersistence") | 
|  | 141 | +                .WithRedisInsight(c => | 
|  | 142 | +                    { | 
|  | 143 | +                        redisInsightBuilder = c; | 
|  | 144 | +                        c.WithLifetime(ContainerLifetime.Persistent); | 
|  | 145 | +                    }); | 
|  | 146 | + | 
|  | 147 | +        // Wire up an additional event subcription to ResourceReadyEvent on the RedisInsightResource | 
|  | 148 | +        // instance. This works because the ResourceReadyEvent fires non-blocking sequential so the | 
|  | 149 | +        // wire-up that WithRedisInsight does is guaranteed to execute before this one does. So we then | 
|  | 150 | +        // use this to block pulling the list of databases until we know they've been updated. This | 
|  | 151 | +        // will repeated below for the second app. | 
|  | 152 | +        // | 
|  | 153 | +        // Issue: https://github.com/dotnet/aspire/issues/6455 | 
|  | 154 | +        Assert.NotNull(redisInsightBuilder); | 
|  | 155 | +        var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); | 
|  | 156 | +        builder1.Eventing.Subscribe<ResourceReadyEvent>(redisInsightBuilder.Resource, (evt, ct) => | 
|  | 157 | +        { | 
|  | 158 | +            redisInsightsReady.TrySetResult(); | 
|  | 159 | +            return Task.CompletedTask; | 
|  | 160 | +        }); | 
|  | 161 | + | 
|  | 162 | +        using var app1 = builder1.Build(); | 
|  | 163 | + | 
|  | 164 | +        await app1.StartAsync(cts.Token); | 
|  | 165 | + | 
|  | 166 | +        await redisInsightsReady.Task.WaitAsync(cts.Token); | 
|  | 167 | + | 
|  | 168 | +        using var client1 = app1.CreateHttpClient($"{redis1.Resource.Name}-insight", "http"); | 
|  | 169 | +        var firstRunDatabases = await client1.GetFromJsonAsync<RedisInsightDatabaseModel[]>("/api/databases", cts.Token); | 
|  | 170 | + | 
|  | 171 | +        Assert.NotNull(firstRunDatabases); | 
|  | 172 | +        Assert.Single(firstRunDatabases); | 
|  | 173 | +        Assert.Equal($"{redis1.Resource.Name}", firstRunDatabases[0].Name); | 
|  | 174 | + | 
|  | 175 | +        await app1.StopAsync(cts.Token); | 
|  | 176 | + | 
|  | 177 | +        using var builder2 = TestDistributedApplicationBuilder.Create(configure, testOutputHelper); | 
|  | 178 | +        builder2.Configuration[$"DcpPublisher:ResourceNameSuffix"] = randomResourceSuffix; | 
|  | 179 | + | 
|  | 180 | +        var redis2 = builder2.AddRedis("redisForInsightPersistence") | 
|  | 181 | +                .WithRedisInsight(c => | 
|  | 182 | +                { | 
|  | 183 | +                    redisInsightBuilder = c; | 
|  | 184 | +                    c.WithLifetime(ContainerLifetime.Persistent); | 
|  | 185 | +                }); | 
|  | 186 | + | 
|  | 187 | +        // Wire up an additional event subcription to ResourceReadyEvent on the RedisInsightResource | 
|  | 188 | +        // instance. This works because the ResourceReadyEvent fires non-blocking sequential so the | 
|  | 189 | +        // wire-up that WithRedisInsight does is guaranteed to execute before this one does. So we then | 
|  | 190 | +        // use this to block pulling the list of databases until we know they've been updated. This | 
|  | 191 | +        // will repeated below for the second app. | 
|  | 192 | +        Assert.NotNull(redisInsightBuilder); | 
|  | 193 | +        redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); | 
|  | 194 | +        builder2.Eventing.Subscribe<ResourceReadyEvent>(redisInsightBuilder.Resource, (evt, ct) => | 
|  | 195 | +        { | 
|  | 196 | +            redisInsightsReady.TrySetResult(); | 
|  | 197 | +            return Task.CompletedTask; | 
|  | 198 | +        }); | 
|  | 199 | + | 
|  | 200 | +        using var app2 = builder2.Build(); | 
|  | 201 | +        await app2.StartAsync(cts.Token); | 
|  | 202 | + | 
|  | 203 | +        await redisInsightsReady.Task.WaitAsync(cts.Token); | 
|  | 204 | + | 
|  | 205 | +        using var client2 = app2.CreateHttpClient($"{redisInsightBuilder.Resource.Name}", "http"); | 
|  | 206 | +        var secondRunDatabases = await client2.GetFromJsonAsync<RedisInsightDatabaseModel[]>("/api/databases", cts.Token); | 
|  | 207 | + | 
|  | 208 | +        Assert.NotNull(secondRunDatabases); | 
|  | 209 | +        Assert.Single(secondRunDatabases); | 
|  | 210 | +        Assert.Equal($"{redis2.Resource.Name}", secondRunDatabases[0].Name); | 
|  | 211 | +        Assert.NotEqual(secondRunDatabases.Single().Id, firstRunDatabases.Single().Id); | 
|  | 212 | + | 
|  | 213 | +        // HACK: This is a workaround for the fact that ApplicationExecutor is not a public type. What I have | 
|  | 214 | +        //       done here is I get the latest event from RNS for the insights instance which gives me the resource | 
|  | 215 | +        //       name as known from a DCP perspective. I then use the ApplicationExecutorProxy (introduced with this | 
|  | 216 | +        //       change to call the ApplicationExecutor stop method. The proxy is a public type with an internal | 
|  | 217 | +        //       constructor inside the Aspire.Hosting.Tests package. This is a short term solution for 9.0 to | 
|  | 218 | +        //       make sure that we have good test coverage for WithRedisInsight behavior, but we need a better | 
|  | 219 | +        //       long term solution in 9.x for folks that will want to do things like execute commands against | 
|  | 220 | +        //       resources to stop specific containers. | 
|  | 221 | +        var rns = app2.Services.GetRequiredService<ResourceNotificationService>(); | 
|  | 222 | +        var latestEvent = await rns.WaitForResourceHealthyAsync(redisInsightBuilder.Resource.Name, cts.Token); | 
|  | 223 | +        var executorProxy = app2.Services.GetRequiredService<ApplicationExecutorProxy>(); | 
|  | 224 | +        await executorProxy.StopResourceAsync(latestEvent.ResourceId, cts.Token); | 
|  | 225 | + | 
|  | 226 | +        await app2.StopAsync(cts.Token); | 
|  | 227 | +    } | 
|  | 228 | + | 
| 123 | 229 |     [Fact] | 
| 124 | 230 |     [RequiresDocker] | 
| 125 | 231 |     public async Task VerifyWithRedisInsightImportDatabases() | 
|  | 
0 commit comments