util: Make logging noexcept #17514

pull laanwj wants to merge 13 commits into bitcoin:master from laanwj:2019_11_logging_noexcept changing 9 files +55 −35
  1. laanwj commented at 11:09 AM on November 19, 2019: member

    Make the logging subsystem noexcept. This facilitates changes such as #17507 by making it possible to log in noexcept context.

    First, adds a function tfm::format_noexcept that works like tfm::format for use in noexcept context if the format string and number of arguments are guaranteed to be correct (for example, if it is trivial like "%02x"). It will abort on any tinyformat error. This is used in utility functions, not for the logging strings itself, as formatting exceptions there are internally caught and logged in LogPrintf (for better troubleshooting of the complex arbitrary formatting expressions used there).

    Then, go through the call hierarchy and make functions noexcept after checking them (see individual commits):

    LogInstance
    LogAcceptCategory
      Logger::WillLogCategory
    LogPrintf
      Logger::Enabled
      LogPrintStr
        LogEscapeMessage
        util::ThreadGetInternalName()
        LogTimestampStr
          GetTimeMicros
          FormatISO8601DateTime
          GetMockTime
        fsbridge::fopen
        FileWriteStr
    

    Though I've checked these functions carefully, it is possible that I missed a function down the call tree. Please let me know if so.

    The only functions I'm slightly uncertain about are the boost posix_time functions in GetTimeMicros/GetTimeMillis, but I could not find any documented exceptions in https://www.boost.org/doc/libs/1_71_0/doc/html/date_time/posix_time.html (fixed, thanks to practicalswift)

    (I know the m_print_callbacks can potentially throw, there is as far as I saw no way to guarantee std::function as noexcept. I leave this as an responsibility to who registers it. FWIW, this functionality is only used as part of the test framework.)

    (Also, thinking of it, any kind of allocation in C++ can except with bad_alloc&, including inside string manipulation, which is almost 100% of the code here. If that's an issue, I'm no longer convinced this is possible at all)

  2. laanwj added the label Utils/log/libs on Nov 19, 2019
  3. laanwj force-pushed on Nov 19, 2019
  4. in src/tinyformat.h:1076 in 5f77484d05 outdated
    1071 | +{
    1072 | +    std::ostringstream oss;
    1073 | +    try {
    1074 | +        format(oss, fmt.c_str(), args...);
    1075 | +    } catch(...) {
    1076 | +        abort();
    


    practicalswift commented at 11:28 AM on November 19, 2019:

    Instead of aborting what about returning Unable to format: "…" in case of a format string mistake?

    Perhaps with an ASSUME(false); to make it abort if --enable-debug (674f9d59ba4c1a1d4662e79467cc417a2988cf15 in #16136 gives us ASSUME(…)).


    laanwj commented at 11:33 AM on November 19, 2019:

    This is intended for use in utility functions (say, date-time formatting, string encodings, and whatnot), in neither of these cases it's acceptable to end up with "Unable to format: " in the string! That's much worse than to just terminate the process. It is assumed these functions have proper unit tests.

    No opinion on how to terminate the process, though. I did not use assert(0) because I don't want to rely on assert() not being defined out, if in the future NDEBUG will be allowed.


    practicalswift commented at 12:30 PM on November 19, 2019:

    Oh, sorry: I now re-read the diff and understand that the use of format_noexcept is extremely limited. Makes perfect sense. Sorry for the misunderstanding.


    MarcoFalke commented at 12:40 PM on November 19, 2019:

    I think you don't have to call std::abort in a throwing function that is marked noexcept(true). An exception will std::terminate the program anyway, no?


    laanwj commented at 9:25 AM on November 20, 2019:

    @MarcoFalke correct. I was not sure to make this explicit or not. If you think it's ok to rely on C++'s behavior here, I'll remove the catch entirely (maybe replace it with a comment?).


    MarcoFalke commented at 2:16 PM on November 20, 2019:

    Yes, I believe that the program will always abort, even if someone sets a terminate handler that does not abort: https://en.cppreference.com/w/cpp/error/terminate_handler

    So it should be fine to remove this and add a comment instead.


    laanwj commented at 2:34 PM on November 20, 2019:

    Done

  5. DrahtBot commented at 11:45 AM on November 19, 2019: member

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #16673 (Relog configuration args on debug.log rotation by LarryRuane)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  6. laanwj commented at 11:55 AM on November 19, 2019: member

    FYI, these are the potential tinyformat exceptions:

    TINYFORMAT_ERROR("tinyformat: Cannot convert from argument type to integer for use as variable width or precision");
    TINYFORMAT_ERROR("tinyformat: Not enough conversion specifiers in format string");
    TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable width");
    TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable precision");
    TINYFORMAT_ERROR("tinyformat: the %a and %A conversion specs are not supported");
    TINYFORMAT_ERROR("tinyformat: %n conversion spec not supported");
    TINYFORMAT_ERROR("tinyformat: Conversion spec incorrectly terminated by end of string");
    TINYFORMAT_ERROR("tinyformat: Not enough format arguments");
    TINYFORMAT_ERROR("tinyformat: Too many conversion specifiers in format string");
    

    These all boil down to:

    • Wrong number of arguments in format string
    • Wrong type of arguments provided (compared to argument spec)
    • Malformed or unsupported format specifiers

    As I understand it, assuming a static format string—anything else is very unwise, and not the case here—these can only be caused by bugs in development, not by invalid values at run-time.

    So if a function with tfm::format_noexcept is at least tested once in the unit tests, it can be relied on to never abort.

    I have intentionally not used tfm::format_noexcept for the log message itself, because that usage of tinyformat is much more error-prone.

  7. practicalswift commented at 12:03 PM on November 19, 2019: contributor

    Concept ACK on making logging noexcept

    Some comments:

    The only functions I'm slightly uncertain about are the boost posix_time functions in GetTimeMicros/GetTimeMillis, but I could not find any documented exceptions in https://www.boost.org/doc/libs/1_71_0/doc/html/date_time/posix_time.html

    GetTimeMillis() does boost::posix_time::ptime(boost::gregorian::date(1970,1,1))) which can throw (from the compiler's perspective that is - we know it can't throw with these inputs :)):

    • boost::gregorian::bad_year ("Year is out of valid range: 1400..10000")
    • boost::gregorian::bad_month ("Month number is out of range 1..12")
    • boost::gregorian::bad_day_of_month ("Day of month value is out of range 1..31")
    • boost::gregorian::bad_day_of_month ("Day of month is not valid for year")

    I suggest cherry-picking in https://github.com/bitcoin/bitcoin/pull/13382/commits/afc97c731e407f87fae0385aa5c3a729981ca1aa from the PR #13382 ("util: Don't throw in GetTime{Millis,Micros}(). Mark as noexcept."):

    diff --git a/src/util/time.cpp b/src/util/time.cpp
    index 83a7937d8f61..217bc1e9cdba 100644
    --- a/src/util/time.cpp
    +++ b/src/util/time.cpp
    @@ -37,18 +37,20 @@ int64_t GetMockTime()
         return nMockTime.load(std::memory_order_relaxed);
     }
     
    -int64_t GetTimeMillis()
    +int64_t GetTimeMillis() noexcept
     {
    +    static const boost::posix_time::ptime epoch = boost::posix_time::from_time_t(0);
         int64_t now = (boost::posix_time::microsec_clock::universal_time() -
    -                   boost::posix_time::ptime(boost::gregorian::date(1970,1,1))).total_milliseconds();
    +                   epoch).total_milliseconds();
         assert(now > 0);
         return now;
     }
     
    -int64_t GetTimeMicros()
    +int64_t GetTimeMicros() noexcept
     {
    +    static const boost::posix_time::ptime epoch = boost::posix_time::from_time_t(0);
         int64_t now = (boost::posix_time::microsec_clock::universal_time() -
    -                   boost::posix_time::ptime(boost::gregorian::date(1970,1,1))).total_microseconds();
    +                   epoch).total_microseconds();
         assert(now > 0);
         return now;
     }
    diff --git a/src/util/time.h b/src/util/time.h
    index f2e27474342b..ae618165063a 100644
    --- a/src/util/time.h
    +++ b/src/util/time.h
    @@ -20,8 +20,8 @@
      */
     
     int64_t GetTime();
    -int64_t GetTimeMillis();
    -int64_t GetTimeMicros();
    +int64_t GetTimeMillis() noexcept;
    +int64_t GetTimeMicros() noexcept;
     int64_t GetSystemTimeInSeconds(); // Like GetTime(), but not mockable
     void SetMockTime(int64_t nMockTimeIn);
     int64_t GetMockTime();
    

    (Also, thinking of it, any kind of allocation in C++ can except with bad_alloc&, including inside string manipulation, which is almost 100% of the code here. If that's an issue, I'm no longer convinced this is possible at all)

    I don't think we have to worry about bad_alloc. From the C++ Core Guidelines (with editors Bjarne Stroustrup & Herb Sutter):

    "Code that is intended to be perfectly general (like the standard library and other utility code of that sort) needs to support environments where a bad_alloc exception may be handled meaningfully. However, most programs and execution environments cannot meaningfully handle a failure to allocate, and aborting the program is the cleanest and simplest response to an allocation failure in those cases. If you know that your application code cannot respond to an allocation failure, it may be appropriate to add noexcept even on functions that allocate."

  8. laanwj commented at 12:22 PM on November 19, 2019: member

    GetTimeMillis() does boost::posix_time::ptime(boost::gregorian::date(1970,1,1))) which can throw:

    Oh crap. Thanks for noticing. Though, I guess that will never happen with this static input. But I think your commit improves the code anyway.

    Thanks, will cherry-pick that!

    Edit: replaced my commit

  9. laanwj force-pushed on Nov 19, 2019
  10. fanquake commented at 1:25 PM on November 19, 2019: member

    Concept ACK

  11. laanwj force-pushed on Nov 19, 2019
  12. fanquake commented at 3:01 PM on November 27, 2019: member

    It will abort on any tinyformat error.

    Is this still the case? I'm testing 71493f0a55f6fa0a23e00d9d239aa6d8210d89ab with this diff:

         RandAddStaticEnv(hasher);
    -    LogPrintf("Feeding %i bytes of environment data into RNG\n", hasher.Size() - old_size);
    +    LogPrintf("Feeding %a bytes of environment data into RNG\n", hasher.Size() - old_size);
    

    which triggers this TINYFORMAT_ERROR: https://github.com/bitcoin/bitcoin/blob/d8a66626d63135fd245d5afc524b88b9a94d208b/src/tinyformat.h#L750-L751

    bitcoind just prints the error and continues:

    2019-11-27T14:49:55Z Error "tinyformat: the %a and %A conversion specs are not supported" while formatting log message: Feeding %i %a bytes ....
    

    The same thing also happens with 3c8505ca49a429dcc6c79fc3fd5d1e752b2f22e8.

  13. laanwj commented at 8:50 AM on November 28, 2019: member

    Is this still the case? I'm testing 71493f0 with this diff:

    No, not in general. strprintfstill raises tinyformat_error. LogPrintf's behavior for invalid format strings is unchanged (the exception is caught and reported, but does not leak out)

    Note that the paragraph related to the use of tfm::format_noexcept specifically. I even mention in the second post

    I have intentionally not used tfm::format_noexcept for the log message itself, because that usage of tinyformat is much more error-prone.

    What this does is make LogPrintf and various utility functions noexcept (no exceptions are guaranteed to leak out). Nothing more. No observable behavior is supposed to change.

  14. in src/tinyformat.h:1073 in 71493f0a55 outdated
    1068 | + * format string contains no errors. Only errors in the format string and inconsistencies
    1069 | + * between the format string and the type or number of arguments passed in cause tinyformat
    1070 | + * exceptions. It does not depend on the value of the arguments.
    1071 | + */
    1072 | +template<typename... Args>
    1073 | +std::string format_noexcept(const std::string &fmt, const Args&... args) noexcept
    


    MarcoFalke commented at 8:35 PM on December 4, 2019:

    Could make this shorter by removing the copy-pasted body?

    std::string format_noexcept(const std::string &fmt, const Args&... args) noexcept {return format(fmt, args...);}
    

    laanwj commented at 9:03 AM on December 5, 2019:

    it looks really weird like this, having _noexcept that simply calls the normal function


    MarcoFalke commented at 6:21 PM on December 5, 2019:

    But it is exactly what happens here. Inlining format doesn't make it throw less exceptions. The only difference between them is the noexcept keyword, which makes the program terminate immediately should any exception be thrown.


    laanwj commented at 7:40 AM on December 6, 2019:

    yes, that's true


    laanwj commented at 7:42 AM on December 6, 2019:

    OTOH this is only three lines, I'd like to keep the comment too, and this adds yet another layer of indirection with std::string return value, it's kind of marginal to be honest I think it's more readable like this

  15. laanwj commented at 9:05 AM on December 5, 2019: member

    I'm not entirely sure this is worth doing anymore. noexcept works slightly different from what I thought when I wrote this PR. it seems it's perfectly fine to use non-noexcept functions from except context if you expect they don't ever except anyway. I'm not sure what this adds at all. so this is not a requirement for #17507 at all

  16. practicalswift commented at 9:37 AM on December 5, 2019: contributor

    @laanwj

    I think it is worth doing.

    What this adds is a guarantee that logging won't throw any exceptions. That enables the compiler to apply optimisations that otherwise would not be possible (by reducing the number of alternative execution paths), and it also simplifies control flow analysis from the perspective of static analysers and human reviewers.

    This is why Scott Mayer of "Effective C++" fame recommends using noexcept in cases like the one we are discussing:

    The difference between unwinding the call stack and possibly unwinding it has a surprisingly large impact on code generation. In a noexcept function, optimizers need not keep the runtime stack in an unwindable state if an exception would propagate out of the function, nor must they ensure that objects in a noexcept function are destroyed in the inverse order of construction should an exception leave the function.

    The result is more opportunities for optimization, not only within the body of a noexcept function, but also at sites where the function is called. Such flexibility is present only for noexcept functions. Functions with throw() exception specifications lack it, as do functions with no exception specification at all. The situation can be summarized this way:

    RetType function(params) noexcept; // most optimizable
    RetType function(params) throw(); // less optimizable
    RetType function(params); // less optimizable
    

    This alone should provide sufficient motivation to declare functions noexcept whenever you can.

    […]

    Things to Remember

    • noexcept is part of a function’s interface, so callers may depend on it.
    • noexcept functions are more optimizable than non-noexcept functions.
    • noexcept is particularly valuable for the move operations and for swap.
    • Most functions are exception-neutral rather than noexcept

    Since logging is used pretty much everywhere in our code base I think it would be good to add a noexcept guarantee to remove the "potentially throwing" effect that using logging currently has on all call sites (and by extension all call sites directly or indirectly calling said call sites: it bubbles up :)).

    I've noticed that @sipa is a actively introducing noexcept where appropriate: perhaps he might want to chime in :)

  17. util: add tfm::format_noexcept
    Add tfm formatting for noexcept context when the format string is
    checked to be 100% correct for sure.
    791c89cd0b
  18. util: Make LogInstance noexcept
    This potentially constructs a Logger object (with default constructor,
    which doesn't except), otherwise returns the current one from a static.
    
    (the allocation of the Logger object could potentially raise an
    exception if there is not enough memory, but I think it's reasonable to
    terminate the program in that case)
    3d51efbfd6
  19. util: Make FileWriteStr noexcept
    libc function `fwrite` doesn't raise.
    015247de93
  20. util: Make Logger::Enabled noexcept
    Takes a lock (doesn't raise) and checks some local fields (doesn't
    raise).
    2a95dac8d3
  21. util: Make Logger::WillLogCategory and LogAcceptCategory noexcept
    This only takes a lock and checks a field.
    95722b7ea3
  22. util: Make LogEscapeMessage noexcept 314a88b53f
  23. util: Make ThreadGetInternalName noexcept
    This function simply returns a global string, or a fixed empty string.
    f57cc8735c
  24. util: Make FormatISO8601DateTime and FormatISO8601Date noexcept
    Requires using tfm::format_noexcept.
    c2cb5b09d7
  25. util: Make GetMockTime noexcept
    Nothing special to do here, loading from an atomic will not
    raise an exception.
    8c920b95f8
  26. util: Don't throw in GetTime{Millis,Micros}(). Mark as noexcept 1f54283ec9
  27. util: Make LogTimestampStr noexcept
    Requires changing use of `strprintf` to `tfm::format_noexcept`.
    6fd0a72b21
  28. util: Make fsbridge::fopen noexcept
    - libc function fopen does not raise an exception
    - windows function _wfopen does not raise an exception
    - std::wstring_convert has no documented exceptions
    9ac1d50820
  29. util: Make LogPrintStr and LogPrintf noexcept
    Now that all subsidiary functions are noexcept, this can be done.
    1d5b8b66b0
  30. laanwj force-pushed on Dec 6, 2019
  31. laanwj commented at 8:00 AM on December 6, 2019: member

    Squashed the fixup commit and rebased.

  32. laanwj closed this on Jan 29, 2020

  33. DrahtBot locked this on Feb 15, 2022

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-13 15:14 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me