Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

copyable smithy client changes #3236

Merged
merged 13 commits into from
Jan 10, 2025
Merged

copyable smithy client changes #3236

merged 13 commits into from
Jan 10, 2025

Conversation

sbera87
Copy link
Contributor

@sbera87 sbera87 commented Jan 2, 2025

Issue #, if available:

Description of changes:

Check all that applies:

  • Did a review by yourself.
  • Added proper tests to cover this PR. (If tests are not applicable, explain.)
  • Checked if this PR is a breaking (APIs have been changed) change.
  • Checked if this PR will not introduce cross-platform inconsistent behavior.
  • Checked if this PR would require a ReadMe/Wiki update.

Check which platforms you have built SDK on to verify the correctness of this PR.

  • Linux
  • Windows
  • Android
  • MacOS
  • IOS
  • Other Platforms

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@sbera87 sbera87 changed the title copyable client changes copyable smithy client changes Jan 2, 2025
@sbera87 sbera87 requested a review from sbiscigl January 5, 2025 18:42
@@ -27,6 +27,31 @@ namespace Aws
explicit Cache(size_t initialSize = 1000) : m_maxSize(initialSize)
{
}

//this can't be default as compiler wont implicitly create copy constructor for const qualified members
Cache(const Cache& other):m_entries{other.m_entries},m_maxSize{other.m_maxSize}{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are NOT making anything with a cache copy constructible, if the client is not copy constructible from before the smithy client migration, it does not need to be copy constructible afterwards.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, will remove this

@sbera87 sbera87 requested a review from sbiscigl January 6, 2025 15:56
@sbera87 sbera87 requested a review from sbiscigl January 6, 2025 16:20
@@ -131,11 +167,11 @@ namespace client
}

protected:
ServiceClientConfigurationT& m_clientConfiguration;
ServiceClientConfigurationT m_clientConfiguration;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think we want to be removing the refence. i think this might root cause to the conversation we were having about deep vs shallow copy ctor. I think this might be more aptly summarized as "lets not share references between constructs in ClientConfiguration via the default copy constructor".

i.e.

#include <aws/core/Aws.h>

using namespace Aws;

auto main() -> int {
  SDKOptions options;
  options.loggingOptions.logLevel = Aws::Utils::Logging::LogLevel::Debug;
  InitAPI(options);
  {
    Client::ClientConfiguration configuration{};
    // new_configuration shares all the same shared ptrs as configuration!
    Client::ClientConfiguration new_configuration{configuration};
  }
  ShutdownAPI(options);
  return 0;
}

For example in client configuration we have a retry strategy that is a shared pointer

std::shared_ptr<RetryStrategy> retryStrategy = nullptr;

which if we call the default copy constructor on will create a shared reference to the same retry strategy that is owned by the original client.

What we want to do is that the new client owns its own retry strategy. i.e. the retry strategy is initialized from the factory that we provide

/**
 * Retry Strategy factory method. Default is DefaultRetryStrategy (i.e. exponential backoff).
 */
std::function<std::shared_ptr<RetryStrategy>()> retryStrategyCreateFn;

This isnt limited to specifically the retry strategy but the shared pointers in client configuration that are shared on copy.

So I think the high level ask here is "Lets create a deep copy function on ClientConfiguration that returns a client configuration with the factories called instead of shared ownership over the pointers, that is explicitly called when copying the new smithy clients".

Note: ServiceClientConfigurationT isnt constrainted here, and thats likely a miss, but for all intents and purposes it always a child of ClientConfiguration and we can statically assert on that if we want.

I know thats a mouthful of words, this is touching a lot of legacy stuff and dealing with a lot of things we have got to conveniently ignore for a while, but we're really close to having a better and cleaner design with this change for this moving forward!

return *this;
}
AwsSmithyClientT::operator=(std::move(other));
Aws::Client::ClientWithAsyncTemplateMethods<DynamoDBClient>::operator=(std::move(other));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont think you can call std::move on a r value twice without entering UB land.

return *this;
}

DynamoDBClient& operator=(DynamoDBClient&& other)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we even need a custom move ctor/assignment operator? thinking that default would be enough because the target would be the new owner of the all of the resources, that are already intiialized.

thinking default for move should be enough

{
m_serviceName = ServiceNameT;
}

AwsSmithyClientT(const AwsSmithyClientT& other):
AwsSmithyClientBase(Aws::MakeShared<ServiceClientConfigurationT>(ServiceNameT, other.m_clientConfig), other.m_serviceName, Aws::Http::CreateHttpClient(*other.m_clientConfig), Aws::MakeShared<MarshallerT>(other.m_serviceName.c_str())),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the sake of consistency MakeShared should be called with the same log tag, i.e.
Aws::MakeShared<ServiceClientConfigurationT>(ServiceNameT, other.m_clientConfig)
vs
Aws::MakeShared<MarshallerT>(other.m_serviceName.c_str())

other.m_serviceName should be the same ServiceNameT but since this is the class that is calling MakeShared lets make it consistent to ServiceNameT for both

@@ -65,6 +67,33 @@ namespace client
/* Non-template base client class that contains main Aws Client Request pipeline logic */
class SMITHY_API AwsSmithyClientBase
{
private:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets move private definitions after public per the google style guide

A class definition should usually start with a public: section, followed by protected:, then private:. Omit sections that would be empty.

void deepCopyClientConfiguration(const AwsSmithyClientBase& target)
{
//first create copy from target
m_clientConfig = Aws::MakeShared<Aws::Client::ClientConfiguration>(target.m_serviceName.c_str(), *target.m_clientConfig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think this is going to work the way we want it to because of slicing here we declare a pointer of ClientConfiguration not ServiceClientConfigurationT. in AwsSmithyClientT we call it as such

m_clientConfiguration{*static_cast<ServiceClientConfigurationT*>(AwsSmithyClientBase::m_clientConfig.get())},

since this is NOT a ServiceClientConfigurationT we enter UB territory.

trying to think of a creative solution around this let me talk to you offline about this

}

void init() {
if (!m_clientConfig->retryStrategy)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo, we should remove these checks and do an unconditional invocation of the functors because when we create the shared ptr from another, it does a shallow copy of the functors too and then it won't reinitialize

Copy link
Contributor

@sbiscigl sbiscigl Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we can do that. consider the code

#include <aws/core/Aws.h>
#include <aws/core/client/AdaptiveRetryStrategy.h>
#include <aws/dynamodb/DynamoDBClient.h>

using namespace Aws;
using namespace Aws::DynamoDB;

namespace {
 const char* LOG_TAG = "TAG";
}

auto main() -> int {
  SDKOptions options;
  options.loggingOptions.logLevel = Aws::Utils::Logging::LogLevel::Debug;
  InitAPI(options);
  {
      Client::ClientConfiguration configuration{};
      configuration.retryStrategy = Aws::MakeShared<Client::AdaptiveRetryStrategy>(LOG_TAG);
      DynamoDBClient client{};
  }
  ShutdownAPI(options);
  return 0;
}

if we remove the check we would unconditionally write over the users specifed retry strategy with the default factory.

So we need to check for the existence of a user specified entity on the configuration before invoking the factory

as for specifically having different logic for copy constructor vs regular constructor, it makes sense to have the same logic so we dont have to update in two places

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the above example
DynamoDBClient client
This calls the constructor which is supposed to do the initialization of shared ptr members because in the very beginning sp is default nullptr.
Now say we copy the client which will invoke the deepcopy method and first it will shallow copy the shared ptr members and then wont reinitialize it using the factory functions. This means 2 clients will have same copy of shared_ptr to retry strategy for example. But in reality each client should have its own separate retry strategy.

if (!m_clientConfig->retryStrategy) will otherwise always be false on a copy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooooo good catch let me push a update

@sbiscigl sbiscigl force-pushed the smithy_copy branch 3 times, most recently from 4f9a138 to 34ff59f Compare January 9, 2025 20:49
@sbera87 sbera87 requested a review from sbiscigl January 9, 2025 21:02
@sbera87
Copy link
Contributor Author

sbera87 commented Jan 9, 2025

lgtm

@sbiscigl sbiscigl force-pushed the smithy_copy branch 2 times, most recently from 7abb2bf to dd9db61 Compare January 9, 2025 21:54
@sbiscigl sbiscigl merged commit 3f943c6 into main Jan 10, 2025
4 of 5 checks passed
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.

3 participants