-
Notifications
You must be signed in to change notification settings - Fork 8
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
Conversation
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
Because the dictionary features are still on special partitions, use a separate cache from the other integration tests.
I'm reading this but it's quite big, thanks for your patience. |
There was a problem hiding this 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.
Momento/ISimpleCacheClient.cs
Outdated
/// <returns>Task containing the result of the delete operation.</returns> | ||
public CacheDeleteResponse Delete(string cacheName, byte[] key); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
var response = this.grpcManager.Client.DictionaryGet(request, MetadataWithCache(cacheName), deadline: CalculateDeadline()); | ||
return new CacheDictionaryGetResponse(response.DictionaryBody[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this subscript seems.. hopeful.
There was a problem hiding this comment.
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) }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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);
public List<string?> Strings() | ||
{ | ||
throw new NotImplementedException(); | ||
return responses.Select(response => response.String()).ToList(); | ||
} |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 7c84e5a
public string? String() | ||
{ | ||
throw new NotImplementedException(); | ||
if (Bytes == null) | ||
{ | ||
return null; | ||
} | ||
return Encoding.UTF8.GetString(Bytes); | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in ea02843
There was a problem hiding this comment.
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 🎉
Momento/Internal/ScsDataClient.cs
Outdated
protected ByteString Convert(byte[] bytes) | ||
{ | ||
return ByteString.CopyFrom(bytes); | ||
} | ||
|
||
protected ByteString Convert(string s) | ||
{ | ||
return ByteString.CopyFromUtf8(s); | ||
} |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted in 06d2cca
Refact the `Convert` method to be an extension method to byte array and string types. With this change the developer types `ToByteString` to convert the underlying type.
There was a problem hiding this 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.
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. |
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
`CreateClient` is more approachable.
#88
This PR:
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.