fix(#1): ExperionRealtimeService 재진입 방지 플래그 추가
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user