Skip to content

feat: use thread-local curl handle for multi-concurrency support#214

Open
maxday wants to merge 10 commits into
masterfrom
maxday/use-thread-local-curl-for-multiconcurrency
Open

feat: use thread-local curl handle for multi-concurrency support#214
maxday wants to merge 10 commits into
masterfrom
maxday/use-thread-local-curl-for-multiconcurrency

Conversation

@maxday
Copy link
Copy Markdown
Contributor

@maxday maxday commented May 26, 2026

Description of changes:

  • Replace the per-instance CURL* member with a thread_local static handle, enabling safe concurrent invocations across multiple threads in multi-concurrency mode. Each thread gets its own curl handle, initialized once and reused across runtime instances on that thread.
  • Remove curl handle lifecycle management from the runtime class — the handle is now created at thread start and cleaned up automatically on thread exit.
  • Fix clang-tidy warnings surfaced by the newer CI toolchain: add const qualifiers, use = default destructor, make set_curl_post_result_options static, use std::uint8_t base for verbosity enum, move internal helper to anonymous namespace, and add missing direct
    includes.
  • Add unit tests covering thread-local behavior (concurrent runtimes on different threads, multiple runtimes on same thread) and existing public API surface (invocation_response, http::response, outcome, invocation_request).
  • Remove no-op placeholder test. No longer needed now that real unit tests are in place.

Test result - GitHub action is ✅
Test result - locally:

Test project /local/home/maxday/cpp-analysis/aws-lambda-cpp/build
      Start  1: ThreadLocalCurl.runtime_construction_succeeds
 1/33 Test  #1: ThreadLocalCurl.runtime_construction_succeeds ..................................   Passed    0.01 sec
      Start  2: ThreadLocalCurl.multiple_runtimes_on_same_thread_do_not_crash
 2/33 Test  #2: ThreadLocalCurl.multiple_runtimes_on_same_thread_do_not_crash ..................   Passed    0.01 sec
      Start  3: ThreadLocalCurl.concurrent_runtimes_on_different_threads
 3/33 Test  #3: ThreadLocalCurl.concurrent_runtimes_on_different_threads .......................   Passed    0.01 sec
      Start  4: ThreadLocalCurl.sequential_requests_on_same_runtime
 4/33 Test  #4: ThreadLocalCurl.sequential_requests_on_same_runtime ............................   Passed    0.01 sec
      Start  5: InvocationResponseTest.success_response_has_correct_payload_and_content_type
 5/33 Test  #5: InvocationResponseTest.success_response_has_correct_payload_and_content_type ...   Passed    0.00 sec
      Start  6: InvocationResponseTest.success_response_with_json
 6/33 Test  #6: InvocationResponseTest.success_response_with_json ..............................   Passed    0.00 sec
      Start  7: InvocationResponseTest.success_response_with_empty_payload
 7/33 Test  #7: InvocationResponseTest.success_response_with_empty_payload .....................   Passed    0.00 sec
      Start  8: InvocationResponseTest.failure_response_is_not_success
 8/33 Test  #8: InvocationResponseTest.failure_response_is_not_success .........................   Passed    0.00 sec
      Start  9: InvocationResponseTest.failure_response_contains_error_message
 9/33 Test  #9: InvocationResponseTest.failure_response_contains_error_message .................   Passed    0.00 sec
      Start 10: InvocationResponseTest.failure_response_json_escapes_quotes
10/33 Test #10: InvocationResponseTest.failure_response_json_escapes_quotes ....................   Passed    0.00 sec
      Start 11: InvocationResponseTest.failure_response_json_escapes_backslash
11/33 Test #11: InvocationResponseTest.failure_response_json_escapes_backslash .................   Passed    0.00 sec
      Start 12: InvocationResponseTest.failure_response_json_escapes_newlines
12/33 Test #12: InvocationResponseTest.failure_response_json_escapes_newlines ..................   Passed    0.00 sec
      Start 13: InvocationResponseTest.failure_response_json_escapes_tabs
13/33 Test #13: InvocationResponseTest.failure_response_json_escapes_tabs ......................   Passed    0.00 sec
      Start 14: InvocationResponseTest.failure_response_json_escapes_control_characters
14/33 Test #14: InvocationResponseTest.failure_response_json_escapes_control_characters ........   Passed    0.00 sec
      Start 15: InvocationResponseTest.success_response_with_binary_content_type
15/33 Test #15: InvocationResponseTest.success_response_with_binary_content_type ...............   Passed    0.00 sec
      Start 16: InvocationResponseTest.constructor_based_failure
16/33 Test #16: InvocationResponseTest.constructor_based_failure ...............................   Passed    0.00 sec
      Start 17: HttpResponseTest.add_and_retrieve_header
17/33 Test #17: HttpResponseTest.add_and_retrieve_header .......................................   Passed    0.00 sec
      Start 18: HttpResponseTest.headers_are_lowercased
18/33 Test #18: HttpResponseTest.headers_are_lowercased ........................................   Passed    0.00 sec
      Start 19: HttpResponseTest.has_header_returns_false_for_missing
19/33 Test #19: HttpResponseTest.has_header_returns_false_for_missing ..........................   Passed    0.00 sec
      Start 20: HttpResponseTest.append_body_accumulates
20/33 Test #20: HttpResponseTest.append_body_accumulates .......................................   Passed    0.00 sec
      Start 21: HttpResponseTest.append_body_empty
21/33 Test #21: HttpResponseTest.append_body_empty .............................................   Passed    0.00 sec
      Start 22: HttpResponseTest.set_response_code
22/33 Test #22: HttpResponseTest.set_response_code .............................................   Passed    0.00 sec
      Start 23: HttpResponseTest.multiple_headers
23/33 Test #23: HttpResponseTest.multiple_headers ..............................................   Passed    0.00 sec
      Start 24: OutcomeTest.success_outcome
24/33 Test #24: OutcomeTest.success_outcome ....................................................   Passed    0.00 sec
      Start 25: OutcomeTest.failure_outcome
25/33 Test #25: OutcomeTest.failure_outcome ....................................................   Passed    0.00 sec
      Start 26: OutcomeTest.move_success
26/33 Test #26: OutcomeTest.move_success .......................................................   Passed    0.00 sec
      Start 27: OutcomeTest.move_failure
27/33 Test #27: OutcomeTest.move_failure .......................................................   Passed    0.00 sec
      Start 28: OutcomeTest.with_response_code
28/33 Test #28: OutcomeTest.with_response_code .................................................   Passed    0.00 sec
      Start 29: InvocationRequestTest.get_time_remaining_future_deadline
29/33 Test #29: InvocationRequestTest.get_time_remaining_future_deadline .......................   Passed    0.00 sec
      Start 30: InvocationRequestTest.get_time_remaining_past_deadline
30/33 Test #30: InvocationRequestTest.get_time_remaining_past_deadline .........................   Passed    0.00 sec
      Start 31: InvocationRequestTest.default_fields_are_empty
31/33 Test #31: InvocationRequestTest.default_fields_are_empty .................................   Passed    0.00 sec
      Start 32: VersionTest.version_string_not_empty
32/33 Test #32: VersionTest.version_string_not_empty ...........................................   Passed    0.00 sec
      Start 33: VersionTest.version_format
33/33 Test #33: VersionTest.version_format .....................................................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 33

Label Time Summary:
unit    =   0.15 sec*proc (33 tests)

Total Test time (real) =   0.17 sec

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

maxday added 10 commits May 26, 2026 14:41
Replace the instance member CURL* with a thread_local static CURL*
at namespace scope. This allows the runtime to be safely used from
multiple threads, each getting their own curl handle without needing
separate runtime instances.
Tests cover:
- Runtime construction with thread-local curl handle
- Multiple sequential runtime instances on the same thread
- Concurrent runtime instances on different threads
- Sequential requests on a single runtime instance

Also fixes a bug from the original change: the destructor must not
call curl_easy_cleanup on the thread_local handle, since the handle
outlives any single runtime instance and is shared across all
instances on the same thread.
Include the HTTP response body in the error log when posting the handler
response fails, aiding debugging of Lambda runtime API errors.

Add unit tests covering invocation_response, http::response, outcome,
invocation_request, and version APIs.
Remove response body from the post failure log message to avoid
potential breaking changes in log output format.
Align unit tests with actual API signatures: failure() takes 2 args,
invocation_response constructor takes 3, get_header() returns outcome.
Remove ExcludeHeaderFilter from .clang-tidy as it's unsupported by CI.
Disable performance-enum-size, misc-use-anonymous-namespace, and
misc-include-cleaner which trigger on pre-existing code with newer
clang-tidy versions in CI.
- Remove redundant inline specifier on in-class method
- Add const to variables that are never modified
- Use = default for trivial destructor
- Make set_curl_post_result_options static (no instance members used)
- Add std::uint8_t base type to verbosity enum (performance-enum-size)
- Move get_prefix to anonymous namespace (misc-use-anonymous-namespace)
- Add direct <cstdarg> include in logging.cpp (misc-include-cleaner)
- Remove redundant inline on in-class method (readability-redundant-inline-specifier)
- Add const qualifiers to unmodified variables (misc-const-correctness)
- Use = default for trivial destructor (modernize-use-equals-default)
- Make set_curl_post_result_options static (readability-convert-member-functions-to-static)
No longer needed now that real unit tests are in place.
@maxday maxday marked this pull request as ready for review May 26, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant