FreeAndNil commented on PR #331:
URL: https://github.com/apache/logging-log4net/pull/331#issuecomment-6122214142

   Implementation plan for #331, kept here for the record. Revised after the 
merge so that it describes the change as it landed.
   
   <details>
   <summary>Plan</summary>
   
   # PR #331: stop reading every exception as a hung-up client
   
   ## The defect
   
   `TelnetAppender.SocketHandler.Send`, 
`src/log4net/Appender/TelnetAppender.cs:455`:
   
   ```csharp
   try
   {
     client.Send(message);
   }
   catch (Exception e) when (!e.IsFatal())
   {
     // The client has closed the connection, remove it from our list
     client.Dispose();
     RemoveClient(client);
   }
   ```
   
   Any non-fatal exception is read as "this client hung up", and the client is 
disconnected with
   nothing written anywhere. That is how f013 turned into a mass disconnect: a 
lone surrogate threw
   while encoding, and every connected client was dropped in turn. f013 itself 
is fixed at the source,
   `SocketClient.Send` escapes through 
`ContentEscape.EscapeUnpairedSurrogates`, but the catch that
   misread it is untouched.
   
   The agent instructions already name this: "A catch of `Exception` always 
carries `when
   (!e.IsFatal())`. The filter is not a licence to widen: a catch that handles 
one expected exception
   keeps naming that type."
   
   Two commits: the fix, and a test-only pass over the provoked internal 
logging.
   
   ## Why it is safe to narrow now, and was not before
   
   Since #318 nothing calls `Send` on the logging thread. The path is
   `Append` -> `_sender.TryEnqueue` -> `BackgroundSender<string>` pump -> 
`SendToClients` ->
   `_handler.Send`. The pump wraps the callback, `BackgroundSender.cs:261-264`:
   
   ```csharp
   catch (Exception e) when (!e.IsFatal())
   {
     CountLost();
     Report($"[{_name}] Failed to send a logging event.", e);
   }
   ```
   
   So an exception that escapes `Send` is counted, reported through the 
appender's `ErrorHandler`, and
   the pump carries on. The old reason to swallow everything, that a throw 
would reach the
   application, no longer holds.
   
   ## Commit 1: the change
   
   `SocketHandler.Send`: two catch clauses in the loop over the clients.
   
   - `catch (Exception e) when (e is SocketException or IOException or 
ObjectDisposedException)`
     disconnects, as before: the socket operations, what a `StreamWriter` over 
a `NetworkStream`
     wraps a `SocketException` in, and a client disposed concurrently by 
`Dispose` or a second
     failure. Only these mean the client is gone.
   - `catch (Exception e) when (!e.IsFatal())` is our own bug. It captures the 
first one as an
     `ExceptionDispatchInfo` and the loop goes on, so the remaining clients are 
still served; after
     the loop `failure?.Throw()` raises it with its stack intact, and the pump 
reports it.
   
   The comment in the `SocketClient` constructor, which said that `Send` reads 
a throw as a hung up
   client, now says the escaping keeps a future gap costing one character 
rather than failing the
   write.
   
   `SocketHandler` is `protected`, so a subclass calling `Send` sees those 
exceptions where it saw
   none. Say so in the commit message and the changelog.
   
   ## Commit 1: the test
   
   `src/log4net.Tests/Appender/TelnetAppenderTest.cs`, NUnit 4, real sockets. 
One new test,
   `AnUnexpectedSendFailureKeepsTheClientConnected`: **an unexpected exception 
from a send leaves
   the client connected and still serves the others.** No black-box route 
reaches it, since the three
   narrowed types are the only ones a real socket raises, so drive the handler 
directly through
   `ReflectionExtensions`:
   
   - Construct `SocketHandler` on the loopback address and a free port with
     `NonPublicNestedType("SocketHandler").Construct<IDisposable>(...)`, 
connect two `TcpClient`s and
     wait, with a bounded poll over the private `_clients` list, until both are 
registered.
   - Replace the first client's `_writer` with a `StreamWriter` over an 
`UnflushableStream`, a
     `MemoryStream` whose `Flush` throws `NotSupportedException` while armed. 
The first client fails
     and the second shows that the loop ran to the end.
   - `handler.Invoke("Send", [payload])` must throw with that inner exception, 
the second client
     must receive the payload, read with a bounded loop and a receive timeout, 
and the client list
     must still hold two. Disarm the stream in a `finally`, or disposing the 
writer throws and hides
     whichever assertion failed.
   
   **Revert to fail**, in place, not with `git stash`: with the wide catch 
back, the first client is
   dropped and the throw never comes; with a rethrow inside the loop instead of 
after it, the second
   client never receives. Each half of the change fails the test on its own.
   
   ## The thing to note, not silently accept
   
   `OnlyOnceErrorHandler` sets `IsEnabled = false` after the first error, 
`OnlyOnceErrorHandler.cs:115`.
   So under the default handler a genuine bug on this path is reported once and 
every later one is as
   silent as today. That is project-wide, not specific to this change, and 
fixing it is not part of
   #331.
   
   ## Commit 2: the internal logging that reaches the console
   
   Unrelated to the catch, but test hygiene on the same rule, and small. 
Measured on `net10.0`, the
   suite emits ten `log4net:ERROR` or `WARN` lines that tests provoke on 
purpose and let reach stderr.
   Wrap each in `LogLog.ExecuteWithoutEmittingInternalMessages`, with a one 
line comment saying the
   message is provoked:
   
   - `AdoNetAppenderTest.NoBufferingTest` and `BufferingTest`: no `CommandText` 
and no `Layout`, both
     warned about on purpose. The activation, the configuration and the logging 
calls go inside the
     wrapper, the assertions on the command stay outside.
   - `XmlConfiguratorTest.ConfigureWithUnkownConfigFile` and 
`LogLogTest.LogReceivedAdapter`: the
     `LogReceivedAdapter` is fed either way, so the wrapper only keeps the 
message off the console.
   - `StringFormatTest.TestFormatString`: the deliberately broken format call.
   
   Ten messages down to two. The two left are 
`LogLogTest.EmitInternalMessages`, which asserts the
   console output itself; a comment there says so. Do not wrap a message a test 
does not intend to
   provoke, find out why it is there instead.
   
   Also in this commit: the agent instructions get the `ReflectionExtensions` 
convention from #326,
   replacing the sentence that pointed at the old `BindingFlags` pattern.
   
   ## Changelog
   
   `src/changelog/3.5.0/331-telnet-unexpected-send-failure.xml`, 
`type="fixed"`, both `id` and `link`
   on `<issue>`. Prose, no bullets. Name what it means: an unexpected failure 
while writing to a
   Telnet client is now reported instead of silently disconnecting that client, 
only the three
   exception types disconnect, anything else is raised once the remaining 
clients have been served,
   and a `SocketHandler` subclass calling `Send` sees those exceptions where it 
saw none. Close with
   `(fixed by @FreeAndNil)`; there is no finding id, this came out of the #318 
review.
   
   ## Verification
   
   ```
   dotnet build src/log4net.sln                                    # 0 warnings
   dotnet test src/log4net.Tests/log4net.Tests.csproj
   ```
   
   ## House rules
   
   Branch `Feature/331-telnet-send-failure` off local `master`, not from 
`origin/master`. Leave the
   work unstaged, write the commit messages to `.git/plans/331/commit-1.txt` 
and `commit-2.txt` and
   stop. The maintainer commits.
   
   </details>
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to