티스토리 뷰
Mono 웹 서버와 OWIN 웹 서버의 크래시 이슈 패치
OWIN 웹 서버 크래시
OWIN(Open Web Interface for .NET) 를 이용하여 mono 환경에서 웹 서버를 띄후 특정 명령으로 웹 서버 프로세스가 크래시가 발생한다.
간단한 아래의 OWIN 호스트를 mono 런타임으로 실행한 후 서버 크래시를 발생해 보자.
mono ./OwinConsoleApp1.exe
그리고 터미널을 열어 아래의 명령을 실행해보자.
curl -X POST http://localhost:8080
그러면 아래와 같이 서버 프로세스가 비정상 종료되고 아래와 같은 오류 메시지를 보여준다.
Unhandled Exception:
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.HttpListenerResponse'.
at System.Net.HttpListenerResponse.set_StatusCode (System.Int32 value) [0x00013] in <b4473693dd3c4d45883c574a53529fbe>:0
at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerResponse.End () [0x0001c] in <68f7adf518f945aaa528fe9acf594456>:0
at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerContext.End () [0x00010] in <68f7adf518f945aaa528fe9acf594456>:0
at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerContext.End (System.Exception ex) [0x0001e] in <68f7adf518f945aaa528fe9acf594456>:0
at Microsoft.Owin.Host.HttpListener.OwinHttpListener.ProcessRequestAsync (System.Net.HttpListenerContext context) [0x0019c] in <68f7adf518f945aaa528fe9acf594456>:0
at Microsoft.Owin.Host.HttpListener.OwinHttpListener.ProcessRequestsAsync () [0x00125] in <68f7adf518f945aaa528fe9acf594456>:0
at System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__7_1 (System.Object state) [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context (System.Object state) [0x00007] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00071] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem () [0x00021] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.ThreadPoolWorkQueue.Dispatch () [0x00074] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback () [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0
[ERROR] FATAL UNHANDLED EXCEPTION: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.HttpListenerResponse'.
at System.Net.HttpListenerResponse.set_StatusCode (System.Int32 value) [0x00013] in <b4473693dd3c4d45883c574a53529fbe>:0
at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerResponse.End () [0x0001c] in <68f7adf518f945aaa528fe9acf594456>:0
at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerContext.End () [0x00010] in <68f7adf518f945aaa528fe9acf594456>:0
at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerContext.End (System.Exception ex) [0x0001e] in <68f7adf518f945aaa528fe9acf594456>:0
at Microsoft.Owin.Host.HttpListener.OwinHttpListener.ProcessRequestAsync (System.Net.HttpListenerContext context) [0x0019c] in <68f7adf518f945aaa528fe9acf594456>:0
at Microsoft.Owin.Host.HttpListener.OwinHttpListener.ProcessRequestsAsync () [0x00125] in <68f7adf518f945aaa528fe9acf594456>:0
at System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__7_1 (System.Object state) [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context (System.Object state) [0x00007] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00071] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem () [0x00021] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading.ThreadPoolWorkQueue.Dispatch () [0x00074] in <f759957039b44a0190b1110fdfe3030f>:0
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback () [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0
일반적으로 HTTP Header 의 Content-Length
속성은 반드시 포함되어야 하는 속성이다. (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) HTTP Body 의 Payload 가 있는 경우 무시할 수 있지만, Payload 가 없는 경우 웹 서버는 Length Required
응답코드로 연결을 끊어버린다.
Mono 런타임의 HttpListener
는 이 같은 예외에 대해 <h1>Length Required</h1>
HTML 메시지를 전송한 후 Response
의 객체의 Disposed()
를 호출하고 HTTP 연결을 끊어버린다. 이 코드에서 HTTP 411 Error
를 전송하고, 이 코드에서 Response
를 닫는 것을 알 수 있다. 그러나 OWIN 파이프라인에서 이 요청을 받아 사용자 코드를 실행한 후 Response.StatusCode
에 값을 설정하려고 하니 ObjectDisposedException
이 발생하고 프로세스는 죽어버린다. 이 코드에서 Mono 의 HttpListenerResponse.StatusCode
를 호출하는 이 코드에서 복구할 수 없는 예외가 발생한다.
OWIN 서버 패치
OWIN 서버의 구조를 살펴보면 다행스럽게도 요청을 처리하기 위해 아래의 코드의 생성자에서 대리자(Delegate)에 메서드를 할당한 것을 볼 수 있다. (코드) 그러므로 우리는 이 대리자에 내가 다시 구현한 메서드를 등록해 주면 된다.
internal OwinHttpListener()
{
_listener = new System.Net.HttpListener();
_startNextRequestAsync = new Action(ProcessRequestsAsync);
_startNextRequestError = new Action<Task>(StartNextRequestError);
SetRequestProcessingLimits(DefaultMaxAccepts, DefaultMaxRequests);
}
내부적으로 sealed class, internal, private 으로 정의된 것들이 많기 때문에 요청을 처리하는 구현에서 기존 동작과 일치하도록 처리해야 한다. 그리고 Mono 런타임에 의해 이미 클라이언트 연결에게 응답을 보냈다면 사용자 코드 및 더 이상 파이프라인을 실행하지 않고 중단해야 한다.
아래와 같이 IsEmptyPayloadAndContentLength
메서드에서 오류가 발생하는 경우인지 판단하고, 이미 클라이언트 연결에게 응답을 보냈다면 새로운 요청을 받을 준비를 시킨다.
if (IsEmptyPayloadAndContentLength(context))
{
Interlocked.Decrement(ref currentOutstandingAccepts); // Decrement currentOutstandingAccepts counting.
owinHttpListenerOffloadStartNextRequest(); // New request processing on Task if possible.
continue;
}
아래는 OwinServerFactory
를 구현하여 OwinHttpListener
의 동작을 변경하는 전체 소스 코드이며, 더 이상 프로세스의 크래시가 발생하지 않는다.
이와 관련된 내용으로 OWIN 프로젝트의 구현체인 aspnet/AspNetKatana 공식 프로젝트에 이슈와 PR 을 요청하였다. 그러나 AspNetKatana 는 이 문제에 대해 '공식적으로 mono 를 지원하지 않는다' 고 하였고, 근본적으로 mono 의 문제가 패치되어야 한다고 한다.
일단 mono 는 이 이슈에 대해 해결되지 않았기 때문에 mono 런타임에서 OWIN 웹 서버를 구동할 경우 꾸준히 문제의 소지가 있다.
Mono 공식 저장소의 코드 패치
우선 이 문제의 원인은 Mono 런타임의 구현체의 버그가 맞다. Mono 런타임에서 HTTP 클라이언트에게 오류를 전송하고 응답 객체 메모리를 정리하고 연결을 끊었다면 Request Context 를 OWIN 에게 넘겨주면 안된다. 그러나 Mono 내부의 Request Context 를 다음 파이프라인으로 넘기면서 OWIN 웹 서버에까지 크래시가 발생하는 영향을 준다.
이와 관련하여 Mono 공식 저장소에 'POST/PUT request without Content-Length Header crashes the Process and HttpListener #10435' 이슈가 존재하는 것을 확인 하였다. 2018년 9월에 이슈가 등록되었지만 여전히 해결이 되지 않았다. 그래서 'Fix if already send error to http client, do not callback. #19664' 의 PR 를 요청하여 머지되었다.
우선 이 문제를 해결하기 위해 오류가 발생하는 코드를 아래와 같이 작성했다.
그리고 curl -X POST [http://localhost:8080/](http://localhost:8080/)
로 HTTP 요청을 보내면 아래와 같이 유시한 오류가 발생하는 것을 알 수 있다.
Unhandled Exception:System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.HttpListenerResponse'. at System.Net.HttpListenerResponse.set_ContentLength64 (System.Int64 value) [0x00013] in <b44
73693dd3c4d45883c574a53529fbe>:0
at MonoConsoleApp1.Program.Main (System.String[] args) [0x000ff] in <60aed05f157f4c0c81f93d9221c9a2ec>:0
at MonoConsoleApp1.Program.<Main> (System.String[] args) [0x0000c] in <60aed05f157f4c0c81f93d9221c9a2ec>:0
[ERROR] FATAL UNHANDLED EXCEPTION: System.ObjectDisposedException: Cannot access a disposed obj
ect.
Object name: 'System.Net.HttpListenerResponse'.
at System.Net.HttpListenerResponse.set_ContentLength64 (System.Int64 value) [0x00013] in <b44
73693dd3c4d45883c574a53529fbe>:0
at MonoConsoleApp1.Program.Main (System.String[] args) [0x000ff] in <60aed05f157f4c0c81f93d92
21c9a2ec>:0
at MonoConsoleApp1.Program.<Main> (System.String[] args) [0x0000c] in <60aed05f157f4c0c81f93d9221c9a2ec>:0
Mono 런타임의 HttpListener
의 GetContextAsync
및 GetContext
메서드는 비동기로 동작하는 것을 알 수 있다. 그리고 EndGetContext
메서드는 스레드의 동기화 메서드를 통해 대기하는 것을 알 수 있다. 그럼 ares.AsyncWaitHandle
의 동기화 객체는 ListenerAsyncResult.AsyncWaitHandle
에서 ManualResetEvent
를 생성한다. 그리고 ListernerAsyncResult.Complete
메서드에서 ManualResetEvent.Set
을 호출하는 것을 알 수 있다.
HttpListener.BeginGetContext
에서 큐에서 컨텍스트를 정상적으로 가져오면 동기화 객체를 Set
하고, 그렇지 않으면 다시 wait_queue
에 넣는 동작이 반복된다.
그럼 이제 어디에서 연결을 맺는지 살펴보면 된다. HttpConnection.OnReadInternal
메서드에서 context
에 오류가 없으면 context.Request.FinishInitialization()
메서드를 호출하는데 이 메서드의 내용을 살펴보자. HttpRequest.FinishInitialize
메서드에서 올바른 연결에 대해 쿼리스트링을 생성하는 작업을 하는데 일부 조건에 만족하지 않는 경우 HTTP 클라이언트로 오류를 전송하는 코드를 발견할 수 있다. context.Connection.SendError (null, 411);
이 메서드는 HttpResponse
객체를 Dispose
를 수행하고 연결을 끊도록 내부 구현이 되어 있다.
그러나 HttpConnection.OnReadInternal
에서 HTTP 클라이언트에게 오류를 전송하고 연결이 끊어졌지면 파이프라인을 계속 실행하는 문제가 발생한다. 그래서 이 부분에서 HTTP 클라이언트로 오류 응답을 전송하였다면 더이상 파이프라인이 실행되지 않도록 PR 을 넣어 공식 Mono 저장소에 머지가 되었다.
그리고 Mono 저장소의 릴리즈는 빠른 편이 아니므로 OWIN 웹 서버를 패치하려면 위에 안내한 것처럼 임시방편으로 수정하면 되고, 차기 Mono 릴리즈 버전에서는 이 문제가 해결되니 기다리면 될 것 같다.
'Mono' 카테고리의 다른 글
[mono] mono-service 버그 패치 (0) | 2019.10.14 |
---|---|
[MonoDevelop] 두 번째 한글화 버전 승인완료 (0) | 2016.01.28 |
[MonoDevelop] v5.7.2.2 한글 버전 배포 공지 (0) | 2015.02.25 |
[Mono] Mono 플랫폼에서 데스크탑 응용 프로그램 개발 3가지 방법 (1) | 2014.03.20 |
- Total
- Today
- Yesterday
- ***** MY SOCIAL *****
- [SOCIAL] 페이스북
- [SOCIAL] 팀 블로그 트위터
- .
- ***** MY OPEN SOURCE *****
- [GITHUB] POWERUMC
- .
- ***** MY PUBLISH *****
- [MSDN] e-Book 백서
- .
- ***** MY TOOLS *****
- [VSX] VSGesture for VS2005,200…
- [VSX] VSGesture for VS2010,201…
- [VSX] Comment Helper for VS200…
- [VSX] VSExplorer for VS2005,20…
- [VSX] VSCmd for VS2005,2008
- .
- ***** MY FAVORITES *****
- MSDN 포럼
- MSDN 라이브러리
- Mono Project
- STEN
- 일본 ATMARKIT
- C++ 빌더 포럼
- .
- 팀 파운데이션 서버
- mono
- monodevelop
- 엄준일
- Silverlight
- Visual Studio 2010
- TFS 2010
- MEF
- Visual Studio
- test
- ALM
- Team Foundation Server 2010
- 땡초
- testing
- LINQ
- .NET
- ASP.NET
- Windows 8
- .NET Framework 4.0
- TFS
- c#
- 비주얼 스튜디오 2010
- 비주얼 스튜디오
- Visual Studio 11
- Visual Studio 2008
- Managed Extensibility Framework
- POWERUMC
- Team Foundation Server
- umc
- github