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

feat: dictionary implementation #88

Merged
merged 39 commits into from
Jul 29, 2022
Merged

feat: dictionary implementation #88

merged 39 commits into from
Jul 29, 2022

Conversation

malandis
Copy link
Contributor

@malandis malandis commented Jul 26, 2022

#88

This PR:

  1. Creates a separate incubating client
  2. Implements the core dictionary functionality (DictionarySet, DictionaryGet, DictionaryDelete, DictionaryRemoveField and their multi counterparts).

For the dictionary implementation, we created a separate incubating client. In the future the incubating client will be in a separate repository; for now it is in a separate namespace. In preparation for this we made the incubating client only depend on a client interface. This interface defines the contract that a production client should implement. We chose this pattern as opposed to inheritance or reflection to: (1) loosely couple the incubating client to the production one, and (2) to be able to have client documentation available to both the production and incubating clients.

To limit accessibility of internal code, we refactored to a separate namespace. To share with the incubating client, we increased visibility where necessary.

In preparation for the repository split, the incubating client has its own set of integration tests.

This commit refactors the constructor to pass the host to the control-
and data-client constructors. These constructors in turn pass the host
down to the control- and data-grpc managers, where we build the
URL. That way SimpleCacheClient does not have to build the URL.
For the initial implementation, we did the following:
- Created a separate integration test project to test the incubating
package
- Moved non-public classes from MomentoSdk to a separate
MomentoSdk.Internal namespace
- Made MomentoSdk.Internal classes that need to be re-used in the
incubating code
- Refactored ScsDataClient into base class and derived so the
incubating client can re-use common functionality
@malandis malandis marked this pull request as draft July 26, 2022 15:59
@malandis malandis marked this pull request as ready for review July 27, 2022 15:34
@kvcache
Copy link
Contributor

kvcache commented Jul 27, 2022

I'm reading this but it's quite big, thanks for your patience.
Please note for merging that you will need to adjust the commit message profoundly, as the squash of all these commit messages does not represent the change very well.

@malandis malandis requested a review from kvcache July 27, 2022 19:09
Copy link
Contributor

@kvcache kvcache left a comment

Choose a reason for hiding this comment

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

Nice start - there are a few things I noticed in here; and probably there are things I missed due to the size. More eyes would be good.

Also looks like the names in here aren't matching the proposal in The Document.

Comment on lines 163 to 164
/// <returns>Task containing the result of the delete operation.</returns>
public CacheDeleteResponse Delete(string cacheName, byte[] key);
Copy link
Contributor

Choose a reason for hiding this comment

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

this doesn't return a Task. (many of these comments refer to Task response which is not present)

why do we need these non-task methods again?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this doesn't return a Task. (many of these comments refer to Task response which is not present)

Thanks. Must have had a typo.

why do we need these non-task methods again?

I proposed getting rid of them. It's still a proposal.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see green lines here - can you justify the presence of these non-task methods in the C# sdk?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will revisit in next PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed the comments in 1181245

Comment on lines 103 to 104
var response = this.grpcManager.Client.DictionaryGet(request, MetadataWithCache(cacheName), deadline: CalculateDeadline());
return new CacheDictionaryGetResponse(response.DictionaryBody[0]);
Copy link
Contributor

Choose a reason for hiding this comment

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

this subscript seems.. hopeful.

Copy link
Contributor Author

@malandis malandis Jul 28, 2022

Choose a reason for hiding this comment

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

It's already in a try-except block where an out-of-bounds exception would be caught, then thrown as a ClientSdkException. I left a TODO in b0277bb for our upcoming exception handling UX revamp.


private async Task<CacheDictionaryGetMultiResponse> SendDictionaryGetMultiAsync(string cacheName, string dictionaryName, IEnumerable<ByteString> fields)
{
_DictionaryGetRequest request = new() { DictionaryName = Convert(dictionaryName) };
Copy link
Contributor

Choose a reason for hiding this comment

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

I've been out of c# for a while; implicit new target from context. Nice idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's pretty nifty. The above is equivalent to:

        _DictionaryGetRequest request = new _DictionaryGetRequest();
        request.DictionaryName = Convert(dictionaryName);

Comment on lines 25 to 28
public List<string?> Strings()
{
throw new NotImplementedException();
return responses.Select(response => response.String()).ToList();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if instead of doubling the static memory cost of responses we should let users opt in to that.

Why not return IEnumerable<string?> instead and omit ToList()?

(here and everywhere)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why not return IEnumerable<string?> instead and omit ToList()?

Good call. I think I converted to list when intiially writing tests. Not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7c84e5a

Comment on lines +19 to 26
public string? String()
{
throw new NotImplementedException();
if (Bytes == null)
{
return null;
}
return Encoding.UTF8.GetString(Bytes);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why isn't this a property?

Copy link
Contributor Author

@malandis malandis Jul 27, 2022

Choose a reason for hiding this comment

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

(1) Want to remind users it's doing something non-trivial (string encoding)
(2) String encoding could raise an exception if the user calls String() on something from the cache that wasn't a string. I think it's bad form to have a property that could raise an exception

Copy link
Contributor

Choose a reason for hiding this comment

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

So call it DecodeString or AsString?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll flesh out a spec for the names so they are consistent across SDKs, then sweep through these in a future PR.

{
throw new NotImplementedException();
return (string)field;
Copy link
Contributor

Choose a reason for hiding this comment

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

again with object. This is not how C# does type conversions.

you're going to raise this or something like it whenever your users invoke this method:

[System.InvalidCastException: Unable to cast object of type 'System.Byte[]' to type 'System.String'.]

Don't use object 😄 (here and everywhere)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After discussing what the set response objects should be doing, I removed this code in b3867ee.

}

public string Key(Encoding? encoding = null)
public string FieldToString()
Copy link
Contributor

Choose a reason for hiding this comment

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

Why aren't these properties? (here and everywhere)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 185 to 200
if (cacheName == null)
{
throw new ArgumentNullException(nameof(cacheName));
}
if (dictionaryName == null)
{
throw new ArgumentNullException(nameof(dictionaryName));
}
if (field == null)
{
throw new ArgumentNullException(nameof(field));
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

public static void ArgumentNotNull(object value, string argumentName)
{
  if (value == null)
  {
    throw new ArgumentNullException(argumentName);
  }
}

used like

ArgumentNotNull(cacheName, nameof(cacheName));

or can you just use [NotNull]? https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.notnullattribute?view=net-6.0

either of these strategies will reduce unnecessary boilerplate line noise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Already considered ArgumentNotNull but it's not available in the version we are using 😢 .

I was not aware of the NotNull attribute and will check it out. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ea02843

Copy link
Contributor

Choose a reason for hiding this comment

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

looks much nicer, and removes over 400 lines 🎉

Comment on lines 27 to 35
protected ByteString Convert(byte[] bytes)
{
return ByteString.CopyFrom(bytes);
}

protected ByteString Convert(string s)
{
return ByteString.CopyFromUtf8(s);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe you want to call these AsByteString?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gone a step further and indulged myself in C# features. In 33d84b0 I implemented these as extension methods to byte[] and string.

{
private readonly GrpcChannel channel;
public Scs.ScsClient Client { get; }

private readonly string version = "csharp:" + GetAssembly(typeof(MomentoSdk.Responses.CacheGetResponse)).GetName().Version.ToString();

internal DataGrpcManager(string authToken, string endpoint)
Copy link
Contributor

Choose a reason for hiding this comment

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

endpoint is the correct word for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Even if it's just the domain name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted in 06d2cca

kvcache
kvcache previously approved these changes Jul 28, 2022
Copy link
Contributor

@kvcache kvcache left a comment

Choose a reason for hiding this comment

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

cool, I'm on board.

@cprice404
Copy link
Contributor

I haven't had time to start looking at the code yet - hope to do that right after lunch. But I am 💯 on the description of the choices about how to structure the incubating client, interface, accessors, etc.

@malandis
Copy link
Contributor Author

I haven't had time to start looking at the code yet - hope to do that right after lunch. But I am 100 on the description of the choices about how to structure the incubating client, interface, accessors, etc.

Thank you. I was seeking your feedback about this in particular.

In C# byte array comparisons are by reference. This will likely cause
unexpected behavior to end-users. To make things do what we mean, we
provide a structural comprarer that works on the contents as opposed
to the array reference.

We also provide a `DictionaryEquals` extension that parallels the C#
library's `SetEquals` method. We have to do this because, even if we
provide a structural comparer as described above, this only operates
on the keys, not the values.
cprice404
cprice404 previously approved these changes Jul 28, 2022
Copy link
Contributor

@cprice404 cprice404 left a comment

Choose a reason for hiding this comment

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

Reviewed in more detail. I like what you've done with the interface. 🚢 from me as soon as the more relevant stakeholders are happy :)

using MomentoSdk.Responses;
namespace MomentoSdk;

public interface ISimpleCacheClient : IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

private readonly ISimpleCacheClient simpleCacheClient;
private readonly ScsDataClient dataClient;

public SimpleCacheClient(ISimpleCacheClient simpleCacheClient, string authToken, uint defaultTtlSeconds, uint? dataClientOperationTimeoutMilliseconds = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

not a blocker for this PR, but I would probably expect this constructor to be private, and we would have a public one (or factory method) that handles the construction of the other client implicitly so the user doesn't have to.

Or perhaps you already have a builder somewhere that I haven't seen yet that takes care of this concern.


public class SimpleCacheClientFactory
{
public static SimpleCacheClient Get(string authToken, uint defaultTtlSeconds, uint? dataClientOperationTimeoutMilliseconds = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, here's the builder :) ignore previous comment.

Is this a typical naming convention (*Factory.Get) for builders in CS?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this a typical naming convention (*Factory.Get) for builders in CS?

I poked around and didn't see a consensus. I was learning toward CreateClient as opposed to Get. Thoughts?

@malandis malandis requested a review from kvcache July 28, 2022 23:12
@malandis malandis linked an issue Jul 29, 2022 that may be closed by this pull request
@malandis malandis merged commit c040694 into main Jul 29, 2022
@malandis malandis deleted the feat/dictionary-impl branch July 29, 2022 14:44
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.

dictionary API SDK implementation
3 participants