Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
# ===========================================
# Copy this file to .env and fill in your actual values

ENVIRONMENT=development
DEBUG_MODE=true
LOG_LEVEL=INFO
LOG_FORMAT=json
DATA_PATH=../data
Comment on lines +6 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ENVIRONMENT vs ASPNETCORE_ENVIRONMENT 값 충돌

두 변수가 서로 다른 값을 갖고 있어 실제 실행 환경이 오인될 수 있습니다. 하나로 통일하세요.

-ASPNETCORE_ENVIRONMENT=Production
+ASPNETCORE_ENVIRONMENT=Development

(또는 ENVIRONMENT 키를 제거/주석 처리하고 ASPNETCORE_ENVIRONMENT만 사용)

Also applies to: 50-50

🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 7-7: [UnorderedKey] The DEBUG_MODE key should go before the ENVIRONMENT key

(UnorderedKey)


[warning] 9-9: [UnorderedKey] The LOG_FORMAT key should go before the LOG_LEVEL key

(UnorderedKey)


[warning] 10-10: [UnorderedKey] The DATA_PATH key should go before the DEBUG_MODE key

(UnorderedKey)

🤖 Prompt for AI Agents
In .env.example around lines 6 to 10 (and similarly at lines ~50), there is a
potential conflict between ENVIRONMENT and ASPNETCORE_ENVIRONMENT having
different values; pick one environment variable to be authoritative (preferably
ASPNETCORE_ENVIRONMENT for ASP.NET apps) and either remove or comment out the
duplicate ENVIRONMENT entry, or set both to the same value to avoid mismatched
runtime behavior; update the file so only the chosen key remains (or both match)
and add a brief comment explaining which variable the app reads.


# Database Configuration
DB_CONNECTION_STRING=Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

DB 연결 문자열은 따옴표로 감싸기

특수문자 포함 값은 인용이 안전합니다. dotenv-linter 경고도 해소됩니다.

-DB_CONNECTION_STRING=Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true
+DB_CONNECTION_STRING="Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DB_CONNECTION_STRING=Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true
DB_CONNECTION_STRING="Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true"
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 13-13: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
.env.example around line 13: the DB_CONNECTION_STRING value contains special
characters and should be wrapped in quotes to be safe and satisfy dotenv-linter;
update the line to enclose the entire connection string in double quotes (e.g.
DB_CONNECTION_STRING="Server=...;Password=...;...") so parsers and linters treat
it as a single quoted value.

DB_PASSWORD=YOUR_DB_PASSWORD

# Redis Configuration
REDIS_CONNECTION_STRING=host.docker.internal:6380

# Distributed System Configuration
DISTRIBUTED_MODE=false
SERVER_ID=

# External Services
LLM_BASE_URL=http://host.docker.internal:7930
MEMORY_BASE_URL=http://host.docker.internal:7940
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ _ReSharper*/
# Docker
**/Dockerfile.*
docker-compose.override.yml
.dockerignore.local

# Keep template files but ignore runtime files
!docker-compose.prod.yml
Expand Down
8 changes: 8 additions & 0 deletions ProjectVG.Api/Middleware/WebSocketMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ await socket.SendAsync(
WebSocketMessageType.Text,
true,
cancellationTokenSource.Token);

// 세션 하트비트 업데이트 (Redis TTL 갱신)
try {
await _webSocketService.UpdateSessionHeartbeatAsync(userId);
}
catch (Exception heartbeatEx) {
_logger.LogWarning(heartbeatEx, "세션 하트비트 업데이트 실패: {UserId}", userId);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ProjectVG.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
}

builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddApplicationServices();
builder.Services.AddApplicationServices(builder.Configuration);
builder.Services.AddDevelopmentCors();

// 부하테스트 환경에서 성능 모니터링 서비스 추가
Expand Down
7 changes: 7 additions & 0 deletions ProjectVG.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@
"JWT": {
"Issuer": "ProjectVG",
"Audience": "ProjectVG"
},
"DistributedSystem": {
"Enabled": false,
"ServerId": "api-server-001",
"HeartbeatIntervalSeconds": 30,
"CleanupIntervalMinutes": 5,
"ServerTimeoutMinutes": 2
}
}
36 changes: 30 additions & 6 deletions ProjectVG.Application/ApplicationServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using ProjectVG.Application.Services.Auth;
using ProjectVG.Application.Services.Character;
using ProjectVG.Application.Services.Chat;
Expand All @@ -12,12 +13,14 @@
using ProjectVG.Application.Services.Credit;
using ProjectVG.Application.Services.Users;
using ProjectVG.Application.Services.WebSocket;
using ProjectVG.Application.Services.MessageBroker;
using ProjectVG.Application.Services.Server;

namespace ProjectVG.Application
{
public static class ApplicationServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
// Auth Services
services.AddScoped<IAuthService, AuthService>();
Expand Down Expand Up @@ -69,13 +72,34 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
// Conversation Services
services.AddScoped<IConversationService, ConversationService>();

// Session Services
services.AddSingleton<IConnectionRegistry, ConnectionRegistry>();

// WebSocket Services
services.AddScoped<IWebSocketManager, WebSocketManager>();
// Distributed System Services
AddDistributedServices(services, configuration);

return services;
}

/// <summary>
/// 분산 시스템 관련 서비스 등록
/// </summary>
private static void AddDistributedServices(IServiceCollection services, IConfiguration configuration)
{
var distributedEnabled = configuration.GetValue<bool>("DistributedSystem:Enabled", false);

if (distributedEnabled)
{
// 분산 환경 서비스
services.AddScoped<IMessageBroker, DistributedMessageBroker>();
services.AddScoped<IWebSocketManager, DistributedWebSocketManager>();
}
else
{
// 단일 서버 환경 서비스
services.AddScoped<IMessageBroker, LocalMessageBroker>();
services.AddScoped<IWebSocketManager, WebSocketManager>();
}

// WebSocket 연결 관리
services.AddSingleton<IConnectionRegistry, ConnectionRegistry>();
}
}
}
89 changes: 89 additions & 0 deletions ProjectVG.Application/Models/MessageBroker/BrokerMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Text.Json;

namespace ProjectVG.Application.Models.MessageBroker
{
public class BrokerMessage
{
public string MessageId { get; set; } = Guid.NewGuid().ToString();
public string MessageType { get; set; } = string.Empty;
public string? TargetUserId { get; set; }
public string? TargetServerId { get; set; }
public string? SourceServerId { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string Payload { get; set; } = string.Empty;
public Dictionary<string, string> Headers { get; set; } = new();

public static BrokerMessage CreateUserMessage(string userId, object payload, string? sourceServerId = null)
{
return new BrokerMessage
{
MessageType = "user_message",
TargetUserId = userId,
SourceServerId = sourceServerId,
Payload = JsonSerializer.Serialize(payload),
Headers = new Dictionary<string, string>
{
["content-type"] = "application/json"
}
};
}

public static BrokerMessage CreateServerMessage(string serverId, object payload, string? sourceServerId = null)
{
return new BrokerMessage
{
MessageType = "server_message",
TargetServerId = serverId,
SourceServerId = sourceServerId,
Payload = JsonSerializer.Serialize(payload),
Headers = new Dictionary<string, string>
{
["content-type"] = "application/json"
}
};
}

public static BrokerMessage CreateBroadcastMessage(object payload, string? sourceServerId = null)
{
return new BrokerMessage
{
MessageType = "broadcast_message",
SourceServerId = sourceServerId,
Payload = JsonSerializer.Serialize(payload),
Headers = new Dictionary<string, string>
{
["content-type"] = "application/json"
}
};
}

public T? DeserializePayload<T>()
{
try
{
return JsonSerializer.Deserialize<T>(Payload);
}
catch
{
return default;
}
}

public string ToJson()
{
return JsonSerializer.Serialize(this);
}

public static BrokerMessage? FromJson(string json)
{
try
{
return JsonSerializer.Deserialize<BrokerMessage>(json);
}
catch
{
return null;
}
}
}
}
41 changes: 41 additions & 0 deletions ProjectVG.Application/Models/Server/ServerInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace ProjectVG.Application.Models.Server
{
public class ServerInfo
{
public string ServerId { get; set; } = string.Empty;
public DateTime StartedAt { get; set; }
public DateTime LastHeartbeat { get; set; }
public int ActiveConnections { get; set; }
public string Status { get; set; } = "healthy";
public string? Environment { get; set; }
public string? Version { get; set; }

public ServerInfo()
{
}

public ServerInfo(string serverId)
{
ServerId = serverId;
StartedAt = DateTime.UtcNow;
LastHeartbeat = DateTime.UtcNow;
ActiveConnections = 0;
Status = "healthy";
}

public void UpdateHeartbeat()
{
LastHeartbeat = DateTime.UtcNow;
}

public void UpdateConnectionCount(int count)
{
ActiveConnections = count;
}

public bool IsHealthy(TimeSpan timeout)
{
return DateTime.UtcNow - LastHeartbeat < timeout;
}
}
}
17 changes: 12 additions & 5 deletions ProjectVG.Application/Services/Chat/Handlers/ChatSuccessHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@
using ProjectVG.Application.Models.WebSocket;
using ProjectVG.Application.Services.WebSocket;
using ProjectVG.Application.Services.Credit;
using ProjectVG.Application.Services.MessageBroker;


namespace ProjectVG.Application.Services.Chat.Handlers
{
public class ChatSuccessHandler
{
private readonly ILogger<ChatSuccessHandler> _logger;
private readonly IWebSocketManager _webSocketService;
private readonly IMessageBroker _messageBroker;
private readonly ICreditManagementService _tokenManagementService;

public ChatSuccessHandler(
ILogger<ChatSuccessHandler> logger,
IWebSocketManager webSocketService,
IMessageBroker messageBroker,
ICreditManagementService tokenManagementService)
{
_logger = logger;
_webSocketService = webSocketService;
_messageBroker = messageBroker;
_tokenManagementService = tokenManagementService;
}

Expand Down Expand Up @@ -61,8 +62,14 @@ public async Task HandleAsync(ChatProcessContext context)
var message = ChatProcessResultMessage.FromSegment(segment, requestId)
.WithCreditInfo(tokensUsed, tokensRemaining);
var wsMessage = new WebSocketMessage("chat", message);

await _webSocketService.SendAsync(userId, wsMessage);

_logger.LogInformation("[메시지브로커] 사용자에게 메시지 전송 시작: UserId={UserId}, MessageType={MessageType}, SegmentOrder={Order}, BrokerType={BrokerType}",
userId, wsMessage.Type, segment.Order, _messageBroker.IsDistributed ? "Distributed" : "Local");

await _messageBroker.SendToUserAsync(userId, wsMessage);

_logger.LogInformation("[메시지브로커] 사용자에게 메시지 전송 완료: UserId={UserId}, MessageType={MessageType}, SegmentOrder={Order}",
userId, wsMessage.Type, segment.Order);
}
catch (Exception ex)
{
Expand Down
Loading
Loading