-
Notifications
You must be signed in to change notification settings - Fork 2k
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
IStorageProvider methods parameter grainType from string to Type #1075
Conversation
Hi @inadler, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution! TTYL, DNFBOT; |
@inadler, Thanks for signing the contribution license agreement so quickly! Actual humans will now validate the agreement and then evaluate the PR. |
@sergeybykov, could this PR milestone be changed to an earlier one? I'm not sure when vNext is due but this change, among other things, provides a solution to #1068 that isn't a breaking change. By inspecting the grain type we can infer the original key. |
@shayhatsor This change breaks every single persistence provider. In general, we do not promise backward compatibility, but I'm a bit hesitant with this one. I'd like to understand if such a break really necessary. We are trying to finalize v1.1.0, and breaking all storage providers doesn't sound exciting, especially so late in the v1.1.0. cycle. |
@sergeybykov, grainType in its current state is grain's |
@inadler 👍 |
Stepping back a bit. Why do you need to pass grain type to storage provider exactly? Can't you pass any metadata you need via the state object? The original intent for passing |
@sergeybykov, don't you think that an
Using the real type provides many benefits :
|
@shayhatsor The concern here is that it would create an unhealthy tight coupling with To be clear, the string |
@sergeybykov, I cannot apply:
on system grains or system grain states. |
Why? So long as each grain type has a unique key ( Maybe I'm missing something here. It seems to me that for a robust persistence solution one would have to define quite a bit of metadata, such as DB and table names, ORM maps, sharding info, etc.. Most of this data cannot be inferred from the grain class's |
I'll chime in as I wrote already in Gitter from the perspective of "robust persistence solution". If this looks like being a larger issue, should we open an issue about this and list expectations from various storage points and chop it into features so as we get work underway? If I assume in the following a mainly relational perspective. To clarify, it looks like @sergeybykov looks this more from the perspective of the state class only and @shayhatsor from both the containing grain and the state class itself. To make the picture fuller, I'll list a few points I could see the whole solution to take:
It looks like part of the argument of @shayhatsor here is that it might make sense to write some data to storage with better guarantees than other data and for that he would like to use also the grain type to infer this information. If I understand correctly, there is some state class |
I am of the view that grain storage should focus on storing grain state, not the grain. From this perspective, the primary information provided to storage providers should be the state of the grain and the grain’s identity. Currently this is, respectively, GrainState and GrainReference. Storage provider implementations may need additional metadata for various reasons, including data organization, versioning, and optimization. This extends the required information to be state (GrainState), grain identity (GrainReference), and metadata (?). Currently the metadata provided to the storage provider is the grainType as a string. This is very limited and not extensible. In my opinion, this is at the heart of the solution proposed in this thread. However, the proposed solution arbitrarily increases the amount of information provided to the storage provider, while not providing an extensible solution. As an alternative, I propose the following: Phase 1
This allows us to update the storage pattern without breaking existing providers, but does not (yet) solve the metadata problem. Phase 2
This attribute allows attributes to pass storage provider specific metadata associated with a grain type to the provider. @inadler, is this sufficient to allow you to resolve your storage provider issues? |
@jason-bragg, but if we have the true grain type, we can read any custom attributes. so why do we need to limit the custom attributes to an Orleans specific type? |
That is not a goal, just a consequence of the solution I've proposed. I'm very open to other solutions, but passing in the grain type is a leaky abstraction, because the grain type would only be passed in to get at attributes that have metadata; the metadata is what the component needs, not the grain type. Instead of solving the problem of how to get the metadata, we'd be passing the responsibility to get the metadata to the provider implementer by passing in the grain type and assuming that is sufficient for them to get what they need. In more general terms, what should grain state storage functionality depend on? As I posted, in my view, it needs to know the identity of the grain it is being associated with, the state being stored, and some provider specific metadata. Do you disagree? If so, what am I missing? If not, then we should explore methods of specifying metadata (like the proposed attribute) that does not require a leaky abstraction nor pass the responsibility of assembling the metadata to the provider implementer. |
@jason-bragg, I obviously don't agree that passing the grain type is a "leaky abstraction" or like @sergeybykov said "quick and dirty". IMHO, it's the most simple, clean, straightforward and robust solution to this issue. I believe storage providers should get all the metadata available. |
I was trying to be generic with my pull request but I now think a specific problem would have been more helpful. In my This is actually a deal breaker for me because my Having the type of the grain, allows me:
if I try to get the original type by using |
So system grains are just Rendezvous Pub Sub grains? You can easily solve that problem for that case, by either using a different storage provider for them and your app grains or same storage provider type with different name and differentiating by name. I am not suggesting that instead of passing |
AFAIK currently yes, but we wish to be able to distinguish current and future internal grains.
we thought about it, but then we'll have to do that for every future internal type.
a method like that can come handy, but where would you put it? on the GrainReference class ?
That's exactly the thing. When you pass the type, I agree that it might be too much information for most scenarios, but when you need it, it's there. |
@inadler 👍 |
Good point. I didn't consider that. Since the only identifier available at load time is the GrainReference, data organization is limited to what it provides. I can definitely see how this is a significantly limiting constraint, especially when integrating with pre-existing storage patterns. The metadata solution I proposed would technically afford you the same workaround as passing in the type, but it would be equally kludgy in regards to addressing this issue. How are you using the grain type to get the grain id? |
@jason-bragg, for example: we're checking if the grain is |
If we added a couple of extension methods to |
I think we should extend @shayhatsor , @inadler - for now, just to unblock you, I think you can use what ever hack you find to solve that issue: for example "we're checking if the grain is IGrainWithIntegerKey and then call grainReference.GetPrimaryKeyLong()" or try to get Long Key from Grain Reference and if that throws, try to get Guid. All hacks, but will unblock you, while the bigger discussion about Grain Identities and what should be espoused where is taking place. The discussion about Grain Identities is not a light one, since it has a broad impact on the programming model, so I expect it to take some time until the decision is made. |
@gabikliot , unfortunately, we are still blocked. |
Ok, so just to unblock you, as I understood there are 2 issues:
Does that unblock you? |
@inadler Can you elaborate on the following problem?
|
I think we can do cleaner than that - we can add an extension method |
@gabikliot , I don't think your solution is acceptable because:
I don't think the bellow code is something we would like to use: try { return grain.GetPrimaryKeyLong(); } catch { }
try { return grain.GetPrimaryKey(); } catch { } |
@inadler If we add |
or even better, an enum, such as |
We have GuidCompound and IntegerCompound, unfortunately. |
well, all I meant is the approach, if more values are needed in that enum, we can add them, can't we? |
@inadler , I suggested that hack as a workaround, to unblock you, until a proper solution is implemented. Of course that is unacceptable as a permanent solution. I think I stressed that maybe 5 times, in various places in my comments. If you have other ways to unblock the issue temporarily, even better. |
Let me start by thanking you all for your help with this issue. @sergeybykov, extension methods for identifying the type of the grain can surely help, but with one limitation - knowing that a key is compounded doesn't help us getting it because there is no method that return compounded key. @jdom , @gabikliot I basically ignore the initial (before Grain activation) ReadStateAsync (by ensuring the existence of RawID when Below is the code for whom who may concern: public interface IGenericState
{
object RawKey { get; set; }
object GetValue();
void SetValue(object value);
}
/// <summary> GrainState wrapper with generic type </summary>
[Serializable]
public class GenericState<T> : GrainState, IGenericState
{
/// <summary> The actual state </summary>
public T Value { get; set; }
/// <summary> The actual Grain key </summary>
public object RawKey { get; set; }
public object GetValue() { return Value; }
public void SetValue(object value) { Value = (T) value; }
public override void SetAll(IDictionary<string, object> values) { Value = default(T); }
}
/// <summary> Defines a grain that uses the <see cref="GenericState{T}"/> instead of the <see cref="GrainState"/>. That way there is no need to derive from GrainState and specify a generic type. Also, when setting the state, it sets its value using reference instead of propagating all its properties using reflection</summary>
public class StatefulGrain<T> : Grain<GenericState<T>> where T : class, new()
{
/// <summary> This method became internal to overcome an Orleans limitation.
/// Use <see cref="OnActivateStatefulGrainAsync"/> instead </summary>
public sealed override async Task OnActivateAsync()
{
base.State.RawKey = RawKey;
await ReadStateAsync();
await OnActivateStatefulGrainAsync();
}
/// <summary> This method is called replaces Orleans's OnActivateAsync and is being caled by the Orleans's OnActivateAsync method that is now became internal </summary>
public virtual Task OnActivateStatefulGrainAsync() { return TaskDone.Done; }
protected StatefulGrain() : base()
{ }
protected StatefulGrain(IGrainIdentity identity, IGrainRuntime runtime, T state, IStorage storage)
: base(identity, runtime, new GenericState<T> {Value = state}, storage)
{ }
/// <summary> The grain's state being read from the persistent storage </summary>
public new T State
{
get { return base.State.Value; }
set { base.State.Value = value; }
}
public string Etag
{
get { return base.State.Etag; }
set { base.State.Etag = value; }
}
public object RawKey
{
get
{
if (this is IGrainWithIntegerKey)
return ((IGrainWithIntegerKey)this).GetPrimaryKeyLong();
if (this is IGrainWithStringKey)
return ((IGrainWithStringKey)this).GetPrimaryKeyString();
if (this is IGrainWithGuidKey)
return ((IGrainWithGuidKey)this).GetPrimaryKey();
string keyExt = null;
object primaryKey = null;
if (this is IGrainWithGuidCompoundKey)
primaryKey = ((IGrainWithGuidCompoundKey)this).GetPrimaryKey(out keyExt);
else if (this is IGrainWithIntegerCompoundKey)
primaryKey = ((IGrainWithIntegerCompoundKey)this).GetPrimaryKeyLong(out keyExt);
return new CompoundKey(primaryKey, keyExt);
}
}
}
public struct CompoundKey
{
public readonly object PrimaryKey;
public readonly string KeyExtension;
public CompundedKey(object primaryKey, string keyExt)
{
PrimaryKey = primaryKey;
KeyExtension = keyExt;
}
public override string ToString()
{
return string.Join("|", PrimaryKey, KeyExtension);
}
} |
@inadler I'm glad you are unblocked. This is still a weak spot in our API that we need to make better. One thing that's not clear to me in your code is why you couldn't use I had another idea how to solve this but haven't had time to explore it yet. We are passing an untyped |
@sergeybykov , that would not work, since a grain may implement more than one interface. BTW, in case you are still trying to get into "one identity to rule them all", we already have 2 - |
@inadler , you solution/trick totally makes sense! |
@sergeybykov , I didn't realize GetPrimaryKeyLong(out string keyExt) is for compounded keys - I updated the above code. @gabikliot , of course it's an hack. public async Task ReadStateAsync(string grainType, GrainReference grainReference, GrainState grainState)
{
var documentKey = grainReference.ToKeyString();
if (grainState is IGenericState)
{
var genericState = ((IGenericState)grainState);
if (genericState.RawKey == null) return;
documentKey = genericState.RawKey.ToString();
}
...
} |
That's exactly what I thought you were doing. |
In my
IStorageProvider
implementation, I need to have the grain type in order to have the complete meta data of the given grain. I'm sure others will find this change useful.This is part of @shayhatsor's suggestion #1035 (comment)