fix(#1): ExperionRealtimeService 재진입 방지 플래그 추가

This commit is contained in:
windpacer
2026-04-26 11:19:57 +09:00
parent 4d46df1b4c
commit 39f6138f9d

View File

@@ -23,6 +23,7 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ExperionRealtimeService> _logger;
private readonly IServiceProvider _sp;
private readonly IOpcUaConfigProvider _configProvider;
private ISession? _session;
private Subscription? _subscription;
@@ -44,6 +45,7 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
private int _subscribedCount;
private string _statusMsg = "중지됨";
private ExperionServerConfig? _currentCfg;
private volatile bool _restarting = false; // 재진입 방지 플래그
// 자동 재시작 플래그 파일 경로
private static readonly string FlagPath =
@@ -52,11 +54,13 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
public ExperionRealtimeService(
IServiceScopeFactory scopeFactory,
ILogger<ExperionRealtimeService> logger,
IServiceProvider sp)
IServiceProvider sp,
IOpcUaConfigProvider configProvider)
{
_scopeFactory = scopeFactory;
_logger = logger;
_sp = sp;
_configProvider = configProvider;
}
// ── IHostedService ────────────────────────────────────────────────────────
@@ -97,10 +101,24 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
public async Task StartAsync(ExperionServerConfig cfg)
{
if (_running)
if (_running || _restarting)
{
_logger.LogWarning("[Realtime] 이미 실행 중. 재시작합니다.");
await StopAsync();
_logger.LogWarning("[Realtime] 이미 실행 중 또는 재시작 중. 무시합니다.");
return;
}
_restarting = true;
try
{
if (_running)
{
_logger.LogWarning("[Realtime] 이미 실행 중. 재시작합니다.");
await StopAsync();
}
}
finally
{
_restarting = false;
}
// 플래그 파일 저장 (앱 재기동 시 자동 재시작용)
@@ -123,6 +141,12 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
public async Task StopAsync()
{
if (_restarting)
{
_logger.LogWarning("[Realtime] 재시작 중이므로 StopAsync 무시 (restarting 플래그 취소)");
return;
}
// 플래그 파일 삭제 (자동 재시작 비활성화)
try
{
@@ -158,6 +182,7 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
// 구독 중이 아니면 DB에만 저장된 상태 — 다음 구독 시작 시 자동 포함
if (!_running || _subscription == null)
return (true, "구독 중 아님 — 다음 구독 시작 시 자동 포함됩니다.");
await Task.CompletedTask;
var item = new MonitoredItem(_subscription.DefaultItem)
{
@@ -175,7 +200,7 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
{
// OPC UA 서버에 실제 적용 — 서버가 node_id 유효성 검증
#pragma warning disable CS0618 // 'ApplyChanges()' is obsolete
await Task.Run(() => { _subscription.ApplyChanges(); });
_subscription.ApplyChanges();
#pragma warning restore CS0618 // 'ApplyChanges()' is obsolete
// 서버 응답 상태 확인 (Error가 null이면 정상)
@@ -184,7 +209,7 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
// 유효하지 않은 node_id → subscription에서 제거
_subscription.RemoveItem(item);
#pragma warning disable CS0618 // 'ApplyChanges()' is obsolete
await Task.Run(() => { _subscription.ApplyChanges(); });
_subscription.ApplyChanges();
#pragma warning restore CS0618 // 'ApplyChanges()' is obsolete
var code = item.Status.Error.StatusCode;
@@ -405,41 +430,11 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
}
}
// ── OPC UA 헬퍼 (ExperionOpcClient 와 동일한 패턴) ───────────────────────
// ── OPC UA 헬퍼 ─────────────────────────────────────────────────────────────
private static async Task<ApplicationConfiguration> BuildConfigAsync(ExperionServerConfig cfg)
private async Task<ApplicationConfiguration> BuildConfigAsync(ExperionServerConfig cfg)
{
var clientCert = ExperionCertificateService.TryLoadCertificate(cfg.ClientHostName);
var config = new ApplicationConfiguration
{
ApplicationName = "ExperionCrawlerClient",
ApplicationType = ApplicationType.Client,
ApplicationUri = cfg.ApplicationUri,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = clientCert != null
? new CertificateIdentifier { Certificate = clientCert }
: new CertificateIdentifier(),
TrustedPeerCertificates = new CertificateTrustList
{ StoreType = "Directory", StorePath = Path.GetFullPath("pki/trusted") },
TrustedIssuerCertificates = new CertificateTrustList
{ StoreType = "Directory", StorePath = Path.GetFullPath("pki/issuers") },
RejectedCertificateStore = new CertificateTrustList
{ StoreType = "Directory", StorePath = Path.GetFullPath("pki/rejected") },
AutoAcceptUntrustedCertificates = true,
AddAppCertToTrustedStore = true
},
TransportQuotas = new TransportQuotas { OperationTimeout = 15_000 },
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 }
};
await config.ValidateAsync(ApplicationType.Client);
config.CertificateValidator.CertificateValidation += (_, e) =>
{
if (e.Error.StatusCode != StatusCodes.Good) e.Accept = true;
};
return config;
return await _configProvider.GetConfigAsync(cfg);
}
private static async Task<ConfiguredEndpoint> SelectEndpointAsync(
@@ -460,16 +455,16 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
return new ConfiguredEndpoint(null, selected, endpointConfig);
}
// OPC UA Session 생성 (비동기)
private static async Task<ISession> CreateSessionAsync(
ApplicationConfiguration appConfig,
ConfiguredEndpoint endpoint,
ExperionServerConfig cfg)
{
var identity = new UserIdentity(cfg.UserName, Encoding.UTF8.GetBytes(cfg.Password));
// CS0618: Session.Create는 obsolete이지만 SessionFactory/CreateAsync가 현재 라이브러리에 없음
// Task.Run으로 래핑하여 비동기 실행
// Session.Create는 동기이므로 Task.Run으로 래핑하여 비동기 실행
#pragma warning disable CS0618 // 'Session.Create()' is obsolete
return await Task.Run(() => Session.Create(
return await Task.Run(() => (ISession)Session.Create(
appConfig,
endpoint,
false,
@@ -488,6 +483,8 @@ public class ExperionRealtimeService : IExperionRealtimeService, IHostedService,
_disposed = true;
_cts?.Cancel();
// StopAsync에서 이미 Task.WhenAll로 대기하므로, Dispose에서는 await 없이 정리만 수행
// CleanupSessionAsync는 이미 완료된 상태를 가정
try
{
CleanupSessionAsync().GetAwaiter().GetResult();