diff --git a/src/Agni.Social/Agni.Social.csproj b/src/Agni.Social/Agni.Social.csproj new file mode 100644 index 0000000..3635410 --- /dev/null +++ b/src/Agni.Social/Agni.Social.csproj @@ -0,0 +1,51 @@ + + + + netstandard2.0 + Agni OS Social Network Assembly + + + + ..\..\out\Debug\ + ..\..\out\Debug\Agni.Social.xml + true + + + + ..\..\out\Release\ + ..\..\out\Release\Agni.Social.xml + true + + + + + + + + + + + + + + + + + + ..\lib\nfx\NFX.dll + + + ..\lib\nfx\NFX.Wave.dll + + + ..\lib\nfx\NFX.Web.dll + + + + + + + + + + diff --git a/src/Agni.Social/Exceptions.cs b/src/Agni.Social/Exceptions.cs new file mode 100644 index 0000000..8704587 --- /dev/null +++ b/src/Agni.Social/Exceptions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Runtime.Serialization; + +using NFX; + +namespace Agni.Social +{ + /// + /// Base exception thrown by the social framework + /// + [Serializable] + public class SocialException : AgniException + { + public SocialException() : base() { } + public SocialException(int code) : base(code) { } + public SocialException(int code, string message) : base(code, message) { } + public SocialException(string message) : base(message) { } + public SocialException(string message, Exception inner) : base(message, inner) { } + public SocialException(string message, Exception inner, int code, string sender, string topic) : base(message, inner, code, sender, topic) { } + protected SocialException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/src/Agni.Social/Graph/Client/GraphCommentManager.cs b/src/Agni.Social/Graph/Client/GraphCommentManager.cs new file mode 100644 index 0000000..91039d4 --- /dev/null +++ b/src/Agni.Social/Graph/Client/GraphCommentManager.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Agni.Coordination; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph +{ + public sealed class GraphCommentManager : GraphCommentManagerBase + { + public GraphCommentManager(HostSet hostSet) : base(hostSet) + { + } + + public override Comment Create(GDID gAuthorNode, GDID gTargetNode, string dimension, string content, byte[] data, + PublicationState publicationState, RatingValue rating = RatingValue.Undefined, DateTime? epoch = null) + { + var pair = HostSet.AssignHost(gTargetNode); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.Create(gAuthorNode, gTargetNode, dimension, content, data, publicationState, rating, epoch), + pair.Select(host => host.RegionPath) + ); + } + + public override Comment Respond(GDID gAuthorNode, CommentID parent, string content, byte[] data) + { + var pair = HostSet.AssignHost(parent.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.Respond(gAuthorNode, parent, content, data), + pair.Select(host => host.RegionPath) + ); + } + + public override GraphChangeStatus Update(CommentID ratingId, RatingValue value, string content, byte[] data) + { + var pair = HostSet.AssignHost(ratingId.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.Update(ratingId, value, content, data), + pair.Select(host => host.RegionPath) + ); + } + + public override GraphChangeStatus DeleteComment(CommentID commentId) + { + var pair = HostSet.AssignHost(commentId.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.DeleteComment(commentId), + pair.Select(host => host.RegionPath) + ); + } + + public override GraphChangeStatus Like(CommentID commentId, int deltaLike, int deltaDislike) + { + var pair = HostSet.AssignHost(commentId.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.Like(commentId, deltaLike, deltaDislike), + pair.Select(host => host.RegionPath) + ); + } + + public override bool IsCommentedByAuthor(GDID gNode, GDID gAuthor, string dimension) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.IsCommentedByAuthor(gNode, gAuthor, dimension), + pair.Select(host => host.RegionPath) + ); + } + + public override IEnumerable GetNodeSummaries(GDID gNode) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub + .CallWithRetry>( + commentSystem => commentSystem.GetNodeSummaries(gNode), + pair.Select(host => host.RegionPath) + ); + } + + public override IEnumerable Fetch(CommentQuery query) + { + var pair = HostSet.AssignHost(query.G_TargetNode); + return Contracts.ServiceClientHub + .CallWithRetry>( + commentSystem => commentSystem.Fetch(query), + pair.Select(host => host.RegionPath) + ); + } + + public override IEnumerable FetchResponses(CommentID commentId) + { + var pair = HostSet.AssignHost(commentId.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry>( + commentSystem => commentSystem.FetchResponses(commentId), + pair.Select(host => host.RegionPath) + ); + } + + public override IEnumerable FetchComplaints(CommentID commentId) + { + var pair = HostSet.AssignHost(commentId.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry>( + commentSystem => commentSystem.FetchComplaints(commentId), + pair.Select(host => host.RegionPath) + ); + } + + public override Comment GetComment(CommentID commentId) + { + var pair = HostSet.AssignHost(commentId.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.GetComment(commentId), + pair.Select(host => host.RegionPath) + ); + } + + public override GraphChangeStatus Complain(CommentID commentId, GDID gAuthorNode, string kind, string message) + { + var pair = HostSet.AssignHost(commentId.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.Complain(commentId, gAuthorNode, kind, message), + pair.Select(host => host.RegionPath) + ); + } + + public override GraphChangeStatus Justify(CommentID commentID) + { + var pair = HostSet.AssignHost(commentID.G_Volume); + return Contracts.ServiceClientHub + .CallWithRetry( + commentSystem => commentSystem.Justify(commentID), + pair.Select(host => host.RegionPath) + ); + } + } + /// + /// Заглушка для интерфейса IGraphCommentSystem на клиенте + /// + public sealed class NOPGraphCommentManager : GraphCommentManagerBase + { + public NOPGraphCommentManager(HostSet hostSet) : base(hostSet) + { + } + + public override Comment Create(GDID gAuthorNode, GDID gTargetNode, string dimension, string content, byte[] data, + PublicationState publicationState, RatingValue rating = RatingValue.Undefined, DateTime? epoch = null) + { + return default(Comment); + } + + public override Comment Respond(GDID gAuthorNode, CommentID parent, string content, byte[] data) + { + return default(Comment); + } + + public override GraphChangeStatus Update(CommentID ratingId, RatingValue value, string content, byte[] data) + { + return GraphChangeStatus.NotFound; + } + + public override GraphChangeStatus DeleteComment(CommentID commentId) + { + return GraphChangeStatus.NotFound; + } + + public override GraphChangeStatus Like(CommentID commentId, int deltaLike, int deltaDislike) + { + return GraphChangeStatus.NotFound; + } + + public override bool IsCommentedByAuthor(GDID gNode, GDID gAuthor, string dimension) + { + return false; + } + + public override IEnumerable GetNodeSummaries(GDID gNode) + { + yield break; + } + + public override IEnumerable Fetch(CommentQuery query) + { + yield break; + } + + public override IEnumerable FetchResponses(CommentID commentId) + { + yield break; + } + + public override IEnumerable FetchComplaints(CommentID commentId) + { + yield break; + } + + public override Comment GetComment(CommentID commentId) + { + return default(Comment); + } + + public override GraphChangeStatus Complain(CommentID commentId, GDID gAuthorNode, string kind, string message) + { + return GraphChangeStatus.NotFound; + } + + public override GraphChangeStatus Justify(CommentID commentID) + { + return GraphChangeStatus.NotFound; + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Client/GraphEventManager.cs b/src/Agni.Social/Graph/Client/GraphEventManager.cs new file mode 100644 index 0000000..3b00bf2 --- /dev/null +++ b/src/Agni.Social/Graph/Client/GraphEventManager.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Agni.Coordination; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph +{ + public sealed class GraphEventManager : GraphEventManagerBase + { + public GraphEventManager(HostSet hostSet) : base(hostSet) + { + } + + public override void EmitEvent(Event evt) + { + var pair = HostSet.AssignHost(evt.G_EmitterNode); + Contracts.ServiceClientHub + .CallWithRetry(eventSystem => eventSystem.EmitEvent(evt), pair.Select(host => host.RegionPath)); + } + + public override void Subscribe(GDID gRecipientNode, GDID gEmitterNode, byte[] parameters) + { + var pair = HostSet.AssignHost(gEmitterNode); + Contracts.ServiceClientHub + .CallWithRetry(eventSystem => eventSystem.Subscribe(gRecipientNode, gEmitterNode, parameters), pair.Select(host => host.RegionPath)); + } + + public override void Unsubscribe(GDID gRecipientNode, GDID gEmitterNode) + { + var pair = HostSet.AssignHost(gEmitterNode); + Contracts.ServiceClientHub + .CallWithRetry(eventSystem => eventSystem.Unsubscribe(gRecipientNode, gEmitterNode), pair.Select(host => host.RegionPath)); + } + + public override long EstimateSubscriberCount(GDID gEmitterNode) + { + var pair = HostSet.AssignHost(gEmitterNode); + return Contracts.ServiceClientHub + .CallWithRetry(eventSystem => eventSystem.EstimateSubscriberCount(gEmitterNode), pair.Select(host => host.RegionPath)); + } + + public override IEnumerable GetSubscribers(GDID gEmitterNode, long start, int count) + { + var pair = HostSet.AssignHost(gEmitterNode); + return Contracts.ServiceClientHub + .CallWithRetry>(eventSystem => eventSystem.GetSubscribers(gEmitterNode, start, count), pair.Select(host => host.RegionPath)); + } + } + + /// + /// Заглушка для интерфейса IGraphEventSystem на клиенте + /// + + public sealed class NOPGraphEventManager : GraphEventManagerBase + { + public NOPGraphEventManager(HostSet hostSet) : base(hostSet) + { + } + + public override void EmitEvent(Event evt) + { + } + + public override void Subscribe(GDID gRecipientNode, GDID gEmitterNode, byte[] parameters) + { + } + + public override void Unsubscribe(GDID gRecipientNode, GDID gEmitterNode) + { + } + + public override long EstimateSubscriberCount(GDID gEmitterNode) + { + return 0; + } + + public override IEnumerable GetSubscribers(GDID gEmitterNode, long start, int count) + { + yield break; + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Client/GraphFriendManager.cs b/src/Agni.Social/Graph/Client/GraphFriendManager.cs new file mode 100644 index 0000000..3544892 --- /dev/null +++ b/src/Agni.Social/Graph/Client/GraphFriendManager.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Agni.Coordination; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph +{ + public class GraphFriendManager : GraphFriendManagerBase + { + public GraphFriendManager(HostSet hostSet) : base(hostSet) + { + } + + public override IEnumerable GetFriendLists(GDID gNode) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub + .CallWithRetry>(friendSystem => friendSystem.GetFriendLists(gNode), pair.Select(host => host.RegionPath)); + } + + public override GraphChangeStatus AddFriendList(GDID gNode, string list, string description) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub + .CallWithRetry(friendSystem => friendSystem.AddFriendList(gNode, list, description), pair.Select(host => host.RegionPath)); + } + + public override GraphChangeStatus DeleteFriendList(GDID gNode, string list) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub + .CallWithRetry(friendSystem => friendSystem.DeleteFriendList(gNode, list), pair.Select(host => host.RegionPath)); + } + + public override IEnumerable GetFriendConnections(FriendQuery query) + { + var pair = HostSet.AssignHost(query.G_Node); + return Contracts.ServiceClientHub + .CallWithRetry>(friendSystem => friendSystem.GetFriendConnections(query), pair.Select(host => host.RegionPath)); + } + + public override GraphChangeStatus AddFriend(GDID gNode, GDID gFriendNode, bool? approve) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub + .CallWithRetry(friendSystem => friendSystem.AddFriend(gNode, gFriendNode, approve), pair.Select(host => host.RegionPath)); + } + + public override GraphChangeStatus AssignFriendLists(GDID gNode, GDID gFriendNode, string lists) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub + .CallWithRetry(friendSystem => friendSystem.AssignFriendLists(gNode, gFriendNode, lists), pair.Select(host => host.RegionPath)); + } + + public override GraphChangeStatus DeleteFriend(GDID gNode, GDID gFriendNode) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub + .CallWithRetry(friendSystem => friendSystem.DeleteFriend(gNode, gFriendNode), pair.Select(host => host.RegionPath)); + } + } + + /// + /// Заглушка для интерфейса IGraphFriendSystem на клиенте + /// + public class NOPGraphFriendManager : GraphFriendManagerBase + { + public NOPGraphFriendManager(HostSet hostSet) : base(hostSet) + { + } + + public override IEnumerable GetFriendLists(GDID gNode) + { + yield break; + } + + public override GraphChangeStatus AddFriendList(GDID gNode, string list, string description) + { + return GraphChangeStatus.NotFound; + } + + public override GraphChangeStatus DeleteFriendList(GDID gNode, string list) + { + return GraphChangeStatus.NotFound; + } + + public override IEnumerable GetFriendConnections(FriendQuery query) + { + yield break; + } + + public override GraphChangeStatus AddFriend(GDID gNode, GDID gFriendNode, bool? approve) + { + return GraphChangeStatus.NotFound; + } + + public override GraphChangeStatus AssignFriendLists(GDID gNode, GDID gFriendNode, string lists) + { + return GraphChangeStatus.NotFound; + } + + public override GraphChangeStatus DeleteFriend(GDID gNode, GDID gFriendNode) + { + return GraphChangeStatus.NotFound; + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Client/GraphManager.cs b/src/Agni.Social/Graph/Client/GraphManager.cs new file mode 100644 index 0000000..143c31a --- /dev/null +++ b/src/Agni.Social/Graph/Client/GraphManager.cs @@ -0,0 +1,115 @@ +using System; +using Agni.Social.Graph.Server; +using Agni.WebMessaging; +using NFX; +using NFX.ApplicationModel; +using NFX.Environment; + +namespace Agni.Social.Graph +{ + /// + /// Фасад для клиента, для работы с IGraphNodeSystem, IGraphCommentSystem, IGraphEventSystem, IGraphFriendSystem + /// + public sealed class GraphManager : DisposableObject, IApplicationStarter, IApplicationFinishNotifiable + { + #region ctor + private static object s_Lock = new object(); + private static volatile GraphManager s_Instance; + + public static GraphManager Instance + { + get + { + var instance = s_Instance; + if (instance == null) + throw new GraphException(StringConsts.GS_INSTANCE_DATA_LAYER_IS_NOT_ALLOCATED_ERROR.Args(typeof(GraphSystemService).Name)); + return instance; + } + } + + private GraphManager() + { + lock (s_Lock) + { + if (s_Instance != null) + throw new GraphException(StringConsts.GS_INSTANCE_ALREADY_ALLOCATED_ERROR.Args(GetType().Name)); + s_Instance = this; + } + } + + protected override void Destructor() + { + lock (s_Lock) + { + base.Destructor(); + s_Instance = null; + } + } + + #endregion + + #region fields + + private IConfigSectionNode m_Config; + + private GraphNodeManagerBase m_Nodes; + private GraphCommentManagerBase m_Comments; + private GraphEventManagerBase m_Events; + private GraphFriendManagerBase m_Friends; + + #endregion + + #region properties + + public IGraphNodeSystem Nodes {get { return m_Nodes; }} + public IGraphCommentSystem Comments {get { return m_Comments; }} + public IGraphEventSystem Events { get { return m_Events; }} + public IGraphFriendSystem Friends {get { return m_Friends; }} + + public bool ApplicationStartBreakOnException {get { return true; } } + public string Name { get { return GetType().Name; } } + + + #endregion + + public void Configure(IConfigSectionNode node) + { + m_Config = node; + } + + public void ApplicationStartBeforeInit(IApplication application) + { + } + /// + /// Не все клиенты могут быть сконфигурированы, если не сконфигурированы, то ставим заглушки + /// + public void ApplicationStartAfterInit(IApplication application) + { + var nodeHostSetAttr = m_Config.AttrByName(SocialConsts.CONFIG_GRAPH_NODE_HOST_SET_ATTR).Value; + var commentHostSetAttr = m_Config.AttrByName(SocialConsts.CONFIG_GRAPH_COMMENT_HOST_SET_ATTR).Value; + var eventHostSetAttr = m_Config.AttrByName(SocialConsts.CONFIG_GRAPH_EVENT_HOST_SET_ATTR).Value; + var friendHostSetAttr = m_Config.AttrByName(SocialConsts.CONFIG_GRAPH_FRIEND_HOST_SET_ATTR).Value; + + + m_Nodes = nodeHostSetAttr != null ? (GraphNodeManagerBase) new GraphNodeManager(AgniSystem.ProcessManager.HostSets[nodeHostSetAttr]) : new NOPGraphNodeManager(null); + m_Comments = commentHostSetAttr != null + ? (GraphCommentManagerBase) new GraphCommentManager(AgniSystem.ProcessManager.HostSets[commentHostSetAttr]) + : new NOPGraphCommentManager(null); + m_Events = eventHostSetAttr != null ? (GraphEventManagerBase) new GraphEventManager(AgniSystem.ProcessManager.HostSets[eventHostSetAttr]) : new NOPGraphEventManager(null); + m_Friends = friendHostSetAttr != null ? (GraphFriendManagerBase) new GraphFriendManager(AgniSystem.ProcessManager.HostSets[friendHostSetAttr]) : new NOPGraphFriendManager(null); + + } + + public void ApplicationFinishBeforeCleanup(IApplication application) + { + DisposableObject.DisposeAndNull(ref m_Nodes); + DisposableObject.DisposeAndNull(ref m_Comments); + DisposableObject.DisposeAndNull(ref m_Events); + DisposableObject.DisposeAndNull(ref m_Friends); + } + + public void ApplicationFinishAfterCleanup(IApplication application) + { + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Client/GraphManagerBase.cs b/src/Agni.Social/Graph/Client/GraphManagerBase.cs new file mode 100644 index 0000000..e4a42f7 --- /dev/null +++ b/src/Agni.Social/Graph/Client/GraphManagerBase.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using Agni.Coordination; +using NFX; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph +{ + /// + /// Базовый класс для всех интерфейсов клиентов + /// Для "подмешивания" IDisposible + /// + public class GraphManagerBase : DisposableObject, IDisposable + { + public GraphManagerBase(HostSet hostSet) + { + m_HostSet = hostSet; + } + + protected HostSet m_HostSet; + + public HostSet HostSet + { + get + { + return m_HostSet; + } + } + + protected override void Destructor() + { + DisposeAndNull(ref m_HostSet); + base.Destructor(); + } + } + + public abstract class GraphNodeManagerBase : GraphManagerBase, IGraphNodeSystem + { + public GraphNodeManagerBase(HostSet hostSet) : base(hostSet) + { + } + + public abstract GraphChangeStatus SaveNode(GraphNode node); + public abstract GraphNode GetNode(GDID gNode); + public abstract GraphChangeStatus DeleteNode(GDID gNode); + public abstract GraphChangeStatus UndeleteNode(GDID gNode); + public abstract GraphChangeStatus RemoveNode(GDID gNode); + } + + public abstract class GraphCommentManagerBase : GraphManagerBase, IGraphCommentSystem + { + public GraphCommentManagerBase(HostSet hostSet) : base(hostSet) + { + } + + public abstract Comment Create(GDID gAuthorNode, GDID gTargetNode, string dimension, string content, byte[] data, + PublicationState publicationState, RatingValue rating = RatingValue.Undefined, DateTime? epoch = null); + + public abstract Comment Respond(GDID gAuthorNode, CommentID parent, string content, byte[] data); + + public abstract GraphChangeStatus Update(CommentID ratingId, RatingValue value, string content, byte[] data); + + public abstract GraphChangeStatus DeleteComment(CommentID commentId); + + public abstract GraphChangeStatus Like(CommentID commentId, int deltaLike, int deltaDislike); + + public abstract bool IsCommentedByAuthor(GDID gNode, GDID gAuthor, string dimension); + + public abstract IEnumerable GetNodeSummaries(GDID gNode); + + public abstract IEnumerable Fetch(CommentQuery query); + + public abstract IEnumerable FetchResponses(CommentID commentId); + + public abstract IEnumerable FetchComplaints(CommentID commentId); + + public abstract Comment GetComment(CommentID commentId); + + public abstract GraphChangeStatus Complain(CommentID commentId, GDID gAuthorNode, string kind, string message); + + public abstract GraphChangeStatus Justify(CommentID commentId); + } + + public abstract class GraphEventManagerBase : GraphManagerBase, IGraphEventSystem + { + public GraphEventManagerBase(HostSet hostSet) : base(hostSet) + { + } + + public abstract void EmitEvent(Event evt); + + public abstract void Subscribe(GDID gRecipientNode, GDID gEmitterNode, byte[] parameters); + + public abstract void Unsubscribe(GDID gRecipientNode, GDID gEmitterNode); + + public abstract long EstimateSubscriberCount(GDID gEmitterNode); + + public abstract IEnumerable GetSubscribers(GDID gEmitterNode, long start, int count); + } + + public abstract class GraphFriendManagerBase : GraphManagerBase, IGraphFriendSystem + { + public GraphFriendManagerBase(HostSet hostSet) : base(hostSet) + { + } + + public abstract IEnumerable GetFriendLists(GDID gNode); + + public abstract GraphChangeStatus AddFriendList(GDID gNode, string list, string description); + + public abstract GraphChangeStatus DeleteFriendList(GDID gNode, string list); + + public abstract IEnumerable GetFriendConnections(FriendQuery query); + + public abstract GraphChangeStatus AddFriend(GDID gNode, GDID gFriendNode, bool? approve); + + public abstract GraphChangeStatus AssignFriendLists(GDID gNode, GDID gFriendNode, string lists); + + public abstract GraphChangeStatus DeleteFriend(GDID gNode, GDID gFriendNode); + } + +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Client/GraphNodeManager.cs b/src/Agni.Social/Graph/Client/GraphNodeManager.cs new file mode 100644 index 0000000..1601be6 --- /dev/null +++ b/src/Agni.Social/Graph/Client/GraphNodeManager.cs @@ -0,0 +1,86 @@ +using System.Linq; + +using NFX; +using NFX.ApplicationModel; +using NFX.DataAccess.Distributed; +using NFX.Environment; + +using Agni.Coordination; + +namespace Agni.Social.Graph +{ + public sealed class GraphNodeManager : GraphNodeManagerBase + { + + public GraphNodeManager(HostSet hostSet) : base(hostSet) + { + + } + + public override GraphChangeStatus SaveNode(GraphNode node) + { + var pair = HostSet.AssignHost(node.GDID); + return Contracts.ServiceClientHub.CallWithRetry(nodeSystem => nodeSystem.SaveNode(node), pair.Select(host => host.RegionPath)); + } + + public override GraphNode GetNode(GDID gNode) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub.CallWithRetry(nodeSystem => nodeSystem.GetNode(gNode), pair.Select(host => host.RegionPath)); + } + + public override GraphChangeStatus DeleteNode(GDID gNode) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub.CallWithRetry(nodeSystem => nodeSystem.DeleteNode(gNode), pair.Select(host => host.RegionPath)); + } + + public override GraphChangeStatus UndeleteNode(GDID gNode) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub.CallWithRetry(nodeSystem => nodeSystem.UndeleteNode(gNode), pair.Select(host => host.RegionPath)); + } + + public override GraphChangeStatus RemoveNode(GDID gNode) + { + var pair = HostSet.AssignHost(gNode); + return Contracts.ServiceClientHub.CallWithRetry(nodeSystem => nodeSystem.RemoveNode(gNode), pair.Select(host => host.RegionPath)); + } + + } + + /// + /// Заглушка для интерфейса IGraphNodeSystem на клиенте + /// + public sealed class NOPGraphNodeManager : GraphNodeManagerBase + { + public NOPGraphNodeManager(HostSet hostSet) : base(hostSet) + { + } + + public override GraphChangeStatus SaveNode(GraphNode node) + { + return GraphChangeStatus.NotFound; + } + + public override GraphNode GetNode(GDID gNode) + { + return default(GraphNode); + } + + public override GraphChangeStatus DeleteNode(GDID gNode) + { + return GraphChangeStatus.NotFound; + } + + public override GraphChangeStatus UndeleteNode(GDID gNode) + { + return GraphChangeStatus.NotFound; + } + + public override GraphChangeStatus RemoveNode(GDID gNode) + { + return GraphChangeStatus.NotFound; + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/CommentID.cs b/src/Agni.Social/Graph/CommentID.cs new file mode 100644 index 0000000..7f8649d --- /dev/null +++ b/src/Agni.Social/Graph/CommentID.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using NFX; +using NFX.DataAccess.Distributed; +using NFX.Serialization.JSON; + +namespace Agni.Social.Graph +{ + /// + /// Represents a read-only tuple of { gVolume: GDID, gComment: GDID}. + /// The gRating is a globally-unique ID however graph system prepends it with + /// gVolume which allows for instant location of a concrete data store which holds gRating. + /// + [Serializable] + public struct CommentID : IEquatable, IJSONWritable + { + public CommentID(GDID gVolume, GDID gComment) { G_Volume = gVolume; G_Comment = gComment; } + + /// + /// Sharding GDID used to instantly find the data store shard where data is kept + /// + public readonly GDID G_Volume; + + /// + ///The global unique id of a comment + /// + public readonly GDID G_Comment; + + /// + /// True if struct is unassigned + /// + public bool Unassigned { get{return G_Volume.IsZero;} } + + /// + /// True if G_Volume | G_Comment isZero + /// + public bool IsZero + { + get { return Unassigned || G_Comment.IsZero; } + } + + public bool Equals(CommentID other) + { + return this.G_Volume == other.G_Volume && + this.G_Comment == other.G_Comment; + } + + public override bool Equals(object obj) + { + if (!(obj is CommentID)) return false; + return this.Equals((CommentID)obj); + } + + public override int GetHashCode() + { + return G_Volume.GetHashCode() ^ G_Comment.GetHashCode(); + } + + public string Stringify() + { + var eLink = new ELink(G_Volume, G_Comment.Bytes); + return eLink.Link; + } + + public override string ToString() + { + return "Comment [{0}@{1}]".Args(G_Volume, G_Comment); + } + + public void WriteAsJSON(TextWriter wri, int nestingLevel, JSONWritingOptions options = null) + { + wri.Write('"'); + wri.Write(Stringify()); + wri.Write('"'); + } + + public static CommentID Parse(string str) + { + CommentID result; + if (!TryParse(str, out result)) + throw new SocialException("CommentID.Parse({0})".Args(str)); + + return result; + } + + public static bool TryParse(string str, out CommentID commentId) + { + try + { + var eLink = new ELink(str); + var gVolume = eLink.GDID; + var gComment = new GDID(eLink.Metadata); + commentId = new CommentID(gVolume, gComment); + return true; + } + catch + { + commentId = default(CommentID); + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/DTO/Comment.cs b/src/Agni.Social/Graph/DTO/Comment.cs new file mode 100644 index 0000000..8fcda0f --- /dev/null +++ b/src/Agni.Social/Graph/DTO/Comment.cs @@ -0,0 +1,106 @@ +using System; +using NFX; + +namespace Agni.Social.Graph +{ + /// + /// Contains social comment data + /// + [Serializable] + public struct Comment + { + /// + /// Comment + /// + /// ID + /// Parent comment ID + /// Author node + /// Target node + /// Creation Date + /// Scope of comments + /// Publication state + /// Rating (0,1,2,3,4,5) + /// Message + /// Data + /// Likes count + /// Dislikes count + /// Complaint count + /// Response count + /// In use (InUse = false - comment has been deleted) + /// Can be edited + public Comment(CommentID id, + CommentID? parentId, + GraphNode authorNode, + GraphNode targetNode, + DateTime createDate, + string dimension, + + PublicationState publicationState, + RatingValue rating, + string message, + byte[] data, + + uint likes, + uint dislikes, + uint complaintCount, + uint responseCount, + + bool inUse, + bool editable) + { + ID = id; + ParentID = parentId; + AuthorNode = authorNode; + TargetNode = targetNode; + Create_Date = createDate; + Dimension = dimension; + + PublicationState = publicationState; + Rating = rating; + Message = message; + Data = data; + + Likes = likes; + Dislikes = dislikes; + ComplaintCount = complaintCount; + ResponseCount = responseCount; + + IsRoot = !parentId.HasValue; + In_Use = inUse; + Editable = editable; + } + + public readonly CommentID ID; + public readonly CommentID? ParentID; + public readonly GraphNode AuthorNode; + public readonly GraphNode TargetNode; + public readonly DateTime Create_Date; + + public readonly bool IsRoot; + public readonly RatingValue Rating; + public readonly string Message; + public readonly byte[] Data; + + public readonly uint Likes; + public readonly uint Dislikes; + public readonly uint ComplaintCount; + public readonly PublicationState PublicationState; + public readonly bool In_Use; + public readonly string Dimension; + public readonly bool Editable; + public readonly uint ResponseCount; + + public override string ToString() + { + return "[{0}-{1}]; [{2}] - {3}; {4}; {5}; ({6}) {7}; Like - {8}; Dislike - {9}".Args(ID.G_Volume, ID.G_Comment, AuthorNode.GDID, AuthorNode.OriginName, TargetNode.GDID, Create_Date, Rating, Message, Likes, Dislikes); + } + + /// + /// Make new Comment + /// + internal static Comment MakeNew(CommentID commentID, CommentID? parentID, GraphNode authorNode, GraphNode targetNode, DateTime create_Date, string dimension, PublicationState publicationState, RatingValue rating, string message, byte[] data) + { + return new Comment(commentID, parentID, authorNode, targetNode, create_Date, dimension, publicationState, rating, message, data, 0, 0, 0, 0, true, true); + } + } +} diff --git a/src/Agni.Social/Graph/DTO/Complaint.cs b/src/Agni.Social/Graph/DTO/Complaint.cs new file mode 100644 index 0000000..220cb03 --- /dev/null +++ b/src/Agni.Social/Graph/DTO/Complaint.cs @@ -0,0 +1,44 @@ +using System; + +using NFX; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph +{ + /// + /// Contains social comment data + /// + [Serializable] + public struct Complaint + { + public Complaint(CommentID commentID, + GDID gComplaint, + GraphNode authorNode, + string kind, + string message, + DateTime createDate, + bool inUse) + { + CommentID = commentID; + GDID = gComplaint; + AuthorNode = authorNode; + Kind = kind; + Message = message; + Create_Date = createDate; + In_Use = inUse; + } + + public readonly CommentID CommentID; + public readonly GDID GDID; + public readonly GraphNode AuthorNode; + public readonly string Kind; + public readonly string Message; + public readonly DateTime Create_Date; + public readonly bool In_Use; + + public override string ToString() + { + return "[{0}-{1}-{2}]: {3}-{4} by {5} ({6})".Args(CommentID.G_Volume, CommentID.G_Comment, GDID, Kind, Message, AuthorNode.OriginName, AuthorNode.GDID); + } + } +} diff --git a/src/Agni.Social/Graph/DTO/SummaryRating.cs b/src/Agni.Social/Graph/DTO/SummaryRating.cs new file mode 100644 index 0000000..1f9bf0e --- /dev/null +++ b/src/Agni.Social/Graph/DTO/SummaryRating.cs @@ -0,0 +1,43 @@ +using System; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph +{ + /// + /// Contains summarized rating information per graph node + /// + [Serializable] + public struct SummaryRating + { + internal SummaryRating(GDID gNode, DateTime createDate, DateTime lastDate, string dimension, ulong count, ulong rating1, ulong rating2, ulong rating3, ulong rating4, ulong rating5) + { + G_Node = gNode; + CreateDate = createDate; + LastChangeDate = lastDate; + Dimension = dimension; + Count = count; + Rating1 = rating1; + Rating2 = rating2; + Rating3 = rating3; + Rating4 = rating4; + Rating5 = rating5; + TotalRatings = Rating1 + Rating2 + Rating3 + Rating4 + Rating5; + + var tr = (float)TotalRatings; + Rating = tr > 0f ? (Rating1 * 1 + Rating2 * 2 + Rating3 * 3 + Rating4 * 4 + Rating5 * 5) / tr : 0f; + } + + public readonly GDID G_Node; + public readonly DateTime CreateDate; + public readonly DateTime LastChangeDate; + public readonly string Dimension; + public readonly ulong Count; + public readonly ulong Rating1; + public readonly ulong Rating2; + public readonly ulong Rating3; + public readonly ulong Rating4; + public readonly ulong Rating5; + public readonly ulong TotalRatings; + public readonly float Rating; + } +} diff --git a/src/Agni.Social/Graph/Enums.cs b/src/Agni.Social/Graph/Enums.cs new file mode 100644 index 0000000..e01f374 --- /dev/null +++ b/src/Agni.Social/Graph/Enums.cs @@ -0,0 +1,108 @@ +namespace Agni.Social.Graph +{ + /// + /// Denotes the results of graph changing operations, such as: Create, Update, Delete + /// + public enum GraphChangeStatus + { + NotFound = -1, + Unassigned = 0, + Added, + Updated, + Deleted + } + + /// + /// Denotes friendship request direction - who requested the friendship (and who approved) + /// + public enum FriendshipRequestDirection + { + /// + /// I requested = G_GraphNode + /// + I = 0, + + /// + /// Friend requested = G_FriendNode + /// + F = 1, Friend = F + } + + /// + /// Denotes friend statuses + /// + public enum FriendStatusFilter + { + Approved = 0, + PendingApproval, + Banned, + All + } + + /// + /// Friend approval status + /// + public enum FriendStatus + { + Pending = 0, + Approved, + Denied, + Banned + } + + public enum FriendVisibility + { + /// + /// Anyone can see friends, even non-logged users/internet/google + /// + Anyone = 0, + + /// + /// Only logged-in users may see friends + /// + Public, + + /// + /// Only firends can see user's friends + /// + Friends, + + /// + /// Only users can see their own friends + /// + Private + } + + /// + /// Defines rating grades + /// + public enum RatingValue + { + Undefined = 0, + Star1 = 1, + Star2 = 2, + Star3 = 3, + Star4 = 4, + Star5 = 5 + } + + + public enum PublicationState + { + //todo Нужны состояния публикации + Private = 0, + Public, + Friend, + Deleted + } + + public enum CommentOrderType + { + ByDate, + ByPositive, + ByNegative, + ByPopular, + ByUsefull + } + +} diff --git a/src/Agni.Social/Graph/Event.cs b/src/Agni.Social/Graph/Event.cs new file mode 100644 index 0000000..da44f1f --- /dev/null +++ b/src/Agni.Social/Graph/Event.cs @@ -0,0 +1,57 @@ +using System; + +using NFX; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph +{ + /// + /// Represents and event that is emitted into the GraphEventSystem + /// + [Serializable] + public struct Event + { + /// + /// Creates a new GraphNode with new GDID + /// + public static Event MakeNew(GDID gEmitterNode, string eType, GDID gTargetShard, GDID gTarget, string config) + { + //todo pereipsat na imnovaniy gdid + var gdid = GDID.Zero;//AgniSystem.GDIDProvider.GenerateOneGDID(SysConsts.GDID_NS_SOCIAL, SysConsts.GDID_NAME_SOCIAL_EVENT); + var utcNow = App.TimeSource.UTCNow; + return new Event(gdid, utcNow, gEmitterNode, eType, gTargetShard, gTarget, config); + } + + internal Event(GDID gdid, DateTime utcTimestamp, GDID gEmitterNode, string eType, GDID gTargetShard, GDID gTarget, string config) + { + GDID = gdid; + TimestampUTC = utcTimestamp; + G_EmitterNode = gEmitterNode; + EventType = eType; + G_TargetShard = gTargetShard; + G_Target = gTarget; + Config = config; + } + + /// Unique ID of the event itself + public readonly GDID GDID; + + /// Event creation UTC timestamp + public readonly DateTime TimestampUTC; + + /// GDID of the emitter node + public readonly GDID G_EmitterNode; + + /// Event type per particular business system + public readonly string EventType; + + /// The sharding gdid of the target (such as a GDID of the user where post is stored) + public readonly GDID G_TargetShard; + + /// The GDID of the target that this event represents, (such as the GDID of the post on user's wall) + public readonly GDID G_Target; + + /// Arbitrary parameters in Laconic format + public readonly string Config; + } +} diff --git a/src/Agni.Social/Graph/Exceptions.cs b/src/Agni.Social/Graph/Exceptions.cs new file mode 100644 index 0000000..6978ef8 --- /dev/null +++ b/src/Agni.Social/Graph/Exceptions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Runtime.Serialization; + +using NFX; + +namespace Agni.Social +{ + /// + /// Base exception thrown by the social Graph framework + /// + [Serializable] + public class GraphException : SocialException + { + public GraphException() : base() { } + public GraphException(int code) : base(code) { } + public GraphException(int code, string message) : base(code, message) { } + public GraphException(string message) : base(message) { } + public GraphException(string message, Exception inner) : base(message, inner) { } + public GraphException(string message, Exception inner, int code, string sender, string topic) : base(message, inner, code, sender, topic) { } + protected GraphException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/src/Agni.Social/Graph/FriendConnection.cs b/src/Agni.Social/Graph/FriendConnection.cs new file mode 100644 index 0000000..7233538 --- /dev/null +++ b/src/Agni.Social/Graph/FriendConnection.cs @@ -0,0 +1,37 @@ +using System; + + +namespace Agni.Social.Graph +{ + /// + /// Returns data about a connected friend + /// + public struct FriendConnection + { + public FriendConnection(GraphNode friend, + DateTime requestDate, + DateTime? approveDate, + FriendshipRequestDirection dir, + FriendVisibility visibility, + string groups) + { + Friend = friend; + RequestDate = requestDate; + ApproveDate = approveDate; + Direction = dir; + Visibility = visibility; + Groups = groups; + } + + public readonly GraphNode Friend; + public readonly DateTime RequestDate; + public readonly DateTime? ApproveDate; + public readonly FriendshipRequestDirection Direction; + public readonly FriendVisibility Visibility; + + /// A comma-delimited list of friend group ids + public readonly string Groups; + + public bool Approved { get { return ApproveDate.HasValue; } } + } +} diff --git a/src/Agni.Social/Graph/GraphCommentSystemClient.cs b/src/Agni.Social/Graph/GraphCommentSystemClient.cs new file mode 100644 index 0000000..7010409 --- /dev/null +++ b/src/Agni.Social/Graph/GraphCommentSystemClient.cs @@ -0,0 +1,446 @@ +//Generated by Agni.Clients.Tools.AgniGluecCompiler + +/* Auto generated by Glue Client Compiler tool (gluec) +on 12/11/2017 10:45:22 PM at LAPCHENKO_DESK by User +Do not modify this file by hand if you plan to regenerate this file again by the tool as manual changes will be lost +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using NFX.Glue; +using NFX.Glue.Protocol; + + +namespace Agni.Social.Graph +{ +// This implementation needs @Agni.@Social.@Graph.@IGraphCommentSystemClient, so +// it can be used with ServiceClientHub class + + /// + /// Client for glued contract Agni.Social.Graph.IGraphCommentSystem server. + /// Each contract method has synchronous and asynchronous versions, the later denoted by 'Async_' prefix. + /// May inject client-level inspectors here like so: + /// client.MsgInspectors.Register( new YOUR_CLIENT_INSPECTOR_TYPE()); + /// + public class GraphCommentSystemClient : ClientEndPoint, @Agni.@Social.@Graph.@IGraphCommentSystemClient + { + + #region Static Members + + private static TypeSpec s_ts_CONTRACT; + private static MethodSpec @s_ms_Create_0; + private static MethodSpec s_ms_Respond_1; + private static MethodSpec @s_ms_Update_2; + private static MethodSpec @s_ms_DeleteComment_3; + private static MethodSpec @s_ms_Like_4; + private static MethodSpec @s_ms_IsCommentedByAuthor_5; + private static MethodSpec @s_ms_GetNodeSummaries_6; + private static MethodSpec @s_ms_Fetch_7; + private static MethodSpec @s_ms_FetchResponses_8; + private static MethodSpec @s_ms_FetchComplaints_9; + private static MethodSpec @s_ms_GetComment_10; + private static MethodSpec @s_ms_Complain_11; + private static MethodSpec @s_ms_Justify_12; + + //static .ctor + static GraphCommentSystemClient() + { + var t = typeof(@Agni.@Social.@Graph.@IGraphCommentSystem); + s_ts_CONTRACT = new TypeSpec(t); + @s_ms_Create_0 = new MethodSpec(t.GetMethod("Create", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@String), typeof(@System.@String), typeof(@System.@Byte[]), typeof(@Agni.@Social.@Graph.@PublicationState), typeof(@Agni.@Social.@Graph.@RatingValue), typeof(@System.@Nullable<@System.@DateTime>) })); + @s_ms_Respond_1 = new MethodSpec(t.GetMethod("Respond", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@Agni.@Social.@Graph.@CommentID), typeof(@System.@String), typeof(@System.@Byte[]) })); + @s_ms_Update_2 = new MethodSpec(t.GetMethod("Update", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentID), typeof(@Agni.@Social.@Graph.@RatingValue), typeof(@System.@String), typeof(@System.@Byte[]) })); + @s_ms_DeleteComment_3 = new MethodSpec(t.GetMethod("DeleteComment", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentID) })); + @s_ms_Like_4 = new MethodSpec(t.GetMethod("Like", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentID), typeof(@System.@Int32), typeof(@System.@Int32) })); + @s_ms_IsCommentedByAuthor_5 = new MethodSpec(t.GetMethod("IsCommentedByAuthor", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@String) })); + @s_ms_GetNodeSummaries_6 = new MethodSpec(t.GetMethod("GetNodeSummaries", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + @s_ms_Fetch_7 = new MethodSpec(t.GetMethod("Fetch", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentQuery) })); + @s_ms_FetchResponses_8 = new MethodSpec(t.GetMethod("FetchResponses", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentID) })); + @s_ms_FetchComplaints_9 = new MethodSpec(t.GetMethod("FetchComplaints", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentID) })); + @s_ms_GetComment_10 = new MethodSpec(t.GetMethod("GetComment", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentID) })); + @s_ms_Complain_11 = new MethodSpec(t.GetMethod("Complain", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentID), typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@String), typeof(@System.@String) })); + @s_ms_Justify_12 = new MethodSpec(t.GetMethod("Justify", new Type[]{ typeof(@Agni.@Social.@Graph.@CommentID) })); + } + #endregion + + #region .ctor + public GraphCommentSystemClient(string node, Binding binding = null) : base(node, binding) { ctor(); } + public GraphCommentSystemClient(Node node, Binding binding = null) : base(node, binding) { ctor(); } + public GraphCommentSystemClient(IGlue glue, string node, Binding binding = null) : base(glue, node, binding) { ctor(); } + public GraphCommentSystemClient(IGlue glue, Node node, Binding binding = null) : base(glue, node, binding) { ctor(); } + + //common instance .ctor body + private void ctor() + { + + } + + #endregion + + public override Type Contract + { + get { return typeof(@Agni.@Social.@Graph.@IGraphCommentSystem); } + } + + + + #region Contract Methods + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Create'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@Comment' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@Comment @Create(@NFX.@DataAccess.@Distributed.@GDID @gAuthorNode, @NFX.@DataAccess.@Distributed.@GDID @gTargetNode, @System.@String @dimension, @System.@String @content, @System.@Byte[] @data, @Agni.@Social.@Graph.@PublicationState @publicationState, @Agni.@Social.@Graph.@RatingValue @value, @System.@Nullable<@System.@DateTime> @timeStamp) + { + var call = Async_Create(@gAuthorNode, @gTargetNode, @dimension, @content, @data, @publicationState, @value, @timeStamp); + return call.GetValue<@Agni.@Social.@Graph.@Comment>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Create'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Create(@NFX.@DataAccess.@Distributed.@GDID @gAuthorNode, @NFX.@DataAccess.@Distributed.@GDID @gTargetNode, @System.@String @dimension, @System.@String @content, @System.@Byte[] @data, @Agni.@Social.@Graph.@PublicationState @publicationState, @Agni.@Social.@Graph.@RatingValue @value, @System.@Nullable<@System.@DateTime> @timeStamp) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Create_0, false, RemoteInstance, new object[]{@gAuthorNode, @gTargetNode, @dimension, @content, @data, @publicationState, @value, @timeStamp}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Response'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@Comment' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@Comment Respond(@NFX.@DataAccess.@Distributed.@GDID @gAuthorNode, @Agni.@Social.@Graph.@CommentID @parent, @System.@String @content, @System.@Byte[] @data) + { + var call = Async_Respond(@gAuthorNode, @parent, @content, @data); + return call.GetValue<@Agni.@Social.@Graph.@Comment>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Response'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Respond(@NFX.@DataAccess.@Distributed.@GDID @gAuthorNode, @Agni.@Social.@Graph.@CommentID @parent, @System.@String @content, @System.@Byte[] @data) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, s_ms_Respond_1, false, RemoteInstance, new object[]{@gAuthorNode, @parent, @content, @data}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Update'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @Update(@Agni.@Social.@Graph.@CommentID @ratingId, @Agni.@Social.@Graph.@RatingValue @value, @System.@String @content, @System.@Byte[] @data) + { + var call = Async_Update(@ratingId, @value, @content, @data); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Update'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Update(@Agni.@Social.@Graph.@CommentID @ratingId, @Agni.@Social.@Graph.@RatingValue @value, @System.@String @content, @System.@Byte[] @data) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Update_2, false, RemoteInstance, new object[]{@ratingId, @value, @content, @data}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.DeleteComment'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @DeleteComment(@Agni.@Social.@Graph.@CommentID @commentId) + { + var call = Async_DeleteComment(@commentId); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.DeleteComment'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_DeleteComment(@Agni.@Social.@Graph.@CommentID @commentId) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_DeleteComment_3, false, RemoteInstance, new object[]{@commentId}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Like'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @Like(@Agni.@Social.@Graph.@CommentID @commentId, @System.@Int32 @deltaLike, @System.@Int32 @deltaDislike) + { + var call = Async_Like(@commentId, @deltaLike, @deltaDislike); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Like'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Like(@Agni.@Social.@Graph.@CommentID @commentId, @System.@Int32 @deltaLike, @System.@Int32 @deltaDislike) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Like_4, false, RemoteInstance, new object[]{@commentId, @deltaLike, @deltaDislike}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.IsCommentedByAuthor'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Boolean' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Boolean @IsCommentedByAuthor(@NFX.@DataAccess.@Distributed.@GDID @gNode, @NFX.@DataAccess.@Distributed.@GDID @gAuthor, @System.@String @dimension) + { + var call = Async_IsCommentedByAuthor(@gNode, @gAuthor, @dimension); + return call.GetValue<@System.@Boolean>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.IsCommentedByAuthor'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_IsCommentedByAuthor(@NFX.@DataAccess.@Distributed.@GDID @gNode, @NFX.@DataAccess.@Distributed.@GDID @gAuthor, @System.@String @dimension) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_IsCommentedByAuthor_5, false, RemoteInstance, new object[]{@gNode, @gAuthor, @dimension}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.GetNodeSummaries'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@SummaryRating>' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@SummaryRating> @GetNodeSummaries(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var call = Async_GetNodeSummaries(@gNode); + return call.GetValue<@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@SummaryRating>>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.GetNodeSummaries'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_GetNodeSummaries(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_GetNodeSummaries_6, false, RemoteInstance, new object[]{@gNode}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Fetch'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Comment>' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Comment> @Fetch(@Agni.@Social.@Graph.@CommentQuery @query) + { + var call = Async_Fetch(@query); + return call.GetValue<@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Comment>>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Fetch'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Fetch(@Agni.@Social.@Graph.@CommentQuery @query) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Fetch_7, false, RemoteInstance, new object[]{@query}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.FetchResponses'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Comment>' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Comment> @FetchResponses(@Agni.@Social.@Graph.@CommentID @commentId) + { + var call = Async_FetchResponses(@commentId); + return call.GetValue<@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Comment>>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.FetchResponses'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_FetchResponses(@Agni.@Social.@Graph.@CommentID @commentId) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_FetchResponses_8, false, RemoteInstance, new object[]{@commentId}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.FetchComplaints'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Complaint>' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Complaint> @FetchComplaints(@Agni.@Social.@Graph.@CommentID @commentId) + { + var call = Async_FetchComplaints(@commentId); + return call.GetValue<@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@Complaint>>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.FetchComplaints'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_FetchComplaints(@Agni.@Social.@Graph.@CommentID @commentId) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_FetchComplaints_9, false, RemoteInstance, new object[]{@commentId}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.GetComment'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@Comment' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@Comment @GetComment(@Agni.@Social.@Graph.@CommentID @commentId) + { + var call = Async_GetComment(@commentId); + return call.GetValue<@Agni.@Social.@Graph.@Comment>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.GetComment'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_GetComment(@Agni.@Social.@Graph.@CommentID @commentId) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_GetComment_10, false, RemoteInstance, new object[]{@commentId}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Complain'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @Complain(@Agni.@Social.@Graph.@CommentID @commentId, @NFX.@DataAccess.@Distributed.@GDID @gAuthorNode, @System.@String @kind, @System.@String @message) + { + var call = Async_Complain(@commentId, @gAuthorNode, @kind, @message); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Complain'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Complain(@Agni.@Social.@Graph.@CommentID @commentId, @NFX.@DataAccess.@Distributed.@GDID @gAuthorNode, @System.@String @kind, @System.@String @message) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Complain_11, false, RemoteInstance, new object[]{@commentId, @gAuthorNode, @kind, @message}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Justify'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @Justify(@Agni.@Social.@Graph.@CommentID @commentID) + { + var call = Async_Justify(@commentID); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphCommentSystem.Justify'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Justify(@Agni.@Social.@Graph.@CommentID @commentID) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Justify_12, false, RemoteInstance, new object[]{@commentID}); + return DispatchCall(request); + } + + + #endregion + + }//class +}//namespace diff --git a/src/Agni.Social/Graph/GraphEventSystemClient.cs b/src/Agni.Social/Graph/GraphEventSystemClient.cs new file mode 100644 index 0000000..0594df3 --- /dev/null +++ b/src/Agni.Social/Graph/GraphEventSystemClient.cs @@ -0,0 +1,214 @@ +//Generated by Agni.Clients.Tools.AgniGluecCompiler + +/* Auto generated by Glue Client Compiler tool (gluec) +on 22.08.2017 19:41:45 at CRAZYROGUE by mad +Do not modify this file by hand if you plan to regenerate this file again by the tool as manual changes will be lost +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using NFX.Glue; +using NFX.Glue.Protocol; + + +namespace Agni.Social.Graph +{ +// This implementation needs @Agni.@Social.@Graph.@IGraphEventSystemClient, so +// it can be used with ServiceClientHub class + + /// + /// Client for glued contract Agni.Social.Graph.IGraphEventSystem server. + /// Each contract method has synchronous and asynchronous versions, the later denoted by 'Async_' prefix. + /// May inject client-level inspectors here like so: + /// client.MsgInspectors.Register( new YOUR_CLIENT_INSPECTOR_TYPE()); + /// + public class GraphEventSystemClient : ClientEndPoint, @Agni.@Social.@Graph.@IGraphEventSystemClient + { + + #region Static Members + + private static TypeSpec s_ts_CONTRACT; + private static MethodSpec @s_ms_EmitEvent_0; + private static MethodSpec @s_ms_Subscribe_1; + private static MethodSpec @s_ms_Unsubscribe_2; + private static MethodSpec @s_ms_EstimateSubscriberCount_3; + private static MethodSpec @s_ms_GetSubscribers_4; + + //static .ctor + static GraphEventSystemClient() + { + var t = typeof(@Agni.@Social.@Graph.@IGraphEventSystem); + s_ts_CONTRACT = new TypeSpec(t); + @s_ms_EmitEvent_0 = new MethodSpec(t.GetMethod("EmitEvent", new Type[]{ typeof(@Agni.@Social.@Graph.@Event) })); + @s_ms_Subscribe_1 = new MethodSpec(t.GetMethod("Subscribe", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@Byte[]) })); + @s_ms_Unsubscribe_2 = new MethodSpec(t.GetMethod("Unsubscribe", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + @s_ms_EstimateSubscriberCount_3 = new MethodSpec(t.GetMethod("EstimateSubscriberCount", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + @s_ms_GetSubscribers_4 = new MethodSpec(t.GetMethod("GetSubscribers", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@Int64), typeof(@System.@Int32) })); + } + #endregion + + #region .ctor + public GraphEventSystemClient(string node, Binding binding = null) : base(node, binding) { ctor(); } + public GraphEventSystemClient(Node node, Binding binding = null) : base(node, binding) { ctor(); } + public GraphEventSystemClient(IGlue glue, string node, Binding binding = null) : base(glue, node, binding) { ctor(); } + public GraphEventSystemClient(IGlue glue, Node node, Binding binding = null) : base(glue, node, binding) { ctor(); } + + //common instance .ctor body + private void ctor() + { + + } + + #endregion + + public override Type Contract + { + get { return typeof(@Agni.@Social.@Graph.@IGraphEventSystem); } + } + + + + #region Contract Methods + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.EmitEvent'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public void @EmitEvent(@Agni.@Social.@Graph.@Event @evt) + { + var call = Async_EmitEvent(@evt); + call.CheckVoidValue(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.EmitEvent'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_EmitEvent(@Agni.@Social.@Graph.@Event @evt) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_EmitEvent_0, false, RemoteInstance, new object[]{@evt}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.Subscribe'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public void @Subscribe(@NFX.@DataAccess.@Distributed.@GDID @gRecipientNode, @NFX.@DataAccess.@Distributed.@GDID @gEmitterNode, @System.@Byte[] @parameters) + { + var call = Async_Subscribe(@gRecipientNode, @gEmitterNode, @parameters); + call.CheckVoidValue(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.Subscribe'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Subscribe(@NFX.@DataAccess.@Distributed.@GDID @gRecipientNode, @NFX.@DataAccess.@Distributed.@GDID @gEmitterNode, @System.@Byte[] @parameters) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Subscribe_1, false, RemoteInstance, new object[]{@gRecipientNode, @gEmitterNode, @parameters}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.Unsubscribe'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public void @Unsubscribe(@NFX.@DataAccess.@Distributed.@GDID @gRecipientNode, @NFX.@DataAccess.@Distributed.@GDID @gEmitterNode) + { + var call = Async_Unsubscribe(@gRecipientNode, @gEmitterNode); + call.CheckVoidValue(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.Unsubscribe'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_Unsubscribe(@NFX.@DataAccess.@Distributed.@GDID @gRecipientNode, @NFX.@DataAccess.@Distributed.@GDID @gEmitterNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Unsubscribe_2, false, RemoteInstance, new object[]{@gRecipientNode, @gEmitterNode}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.EstimateSubscriberCount'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Int64' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Int64 @EstimateSubscriberCount(@NFX.@DataAccess.@Distributed.@GDID @gEmitterNode) + { + var call = Async_EstimateSubscriberCount(@gEmitterNode); + return call.GetValue<@System.@Int64>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.EstimateSubscriberCount'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_EstimateSubscriberCount(@NFX.@DataAccess.@Distributed.@GDID @gEmitterNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_EstimateSubscriberCount_3, false, RemoteInstance, new object[]{@gEmitterNode}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.GetSubscribers'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@GraphNode>' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@GraphNode> @GetSubscribers(@NFX.@DataAccess.@Distributed.@GDID @gEmitterNode, @System.@Int64 @start, @System.@Int32 @count) + { + var call = Async_GetSubscribers(@gEmitterNode, @start, @count); + return call.GetValue<@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@GraphNode>>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphEventSystem.GetSubscribers'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_GetSubscribers(@NFX.@DataAccess.@Distributed.@GDID @gEmitterNode, @System.@Int64 @start, @System.@Int32 @count) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_GetSubscribers_4, false, RemoteInstance, new object[]{@gEmitterNode, @start, @count}); + return DispatchCall(request); + } + + + #endregion + + }//class +}//namespace diff --git a/src/Agni.Social/Graph/GraphFriendSystemClient.cs b/src/Agni.Social/Graph/GraphFriendSystemClient.cs new file mode 100644 index 0000000..1ba137e --- /dev/null +++ b/src/Agni.Social/Graph/GraphFriendSystemClient.cs @@ -0,0 +1,272 @@ +//Generated by Agni.Clients.Tools.AgniGluecCompiler + +/* Auto generated by Glue Client Compiler tool (gluec) +on 8/13/2017 3:50:49 PM at SEXTOD by Anton +Do not modify this file by hand if you plan to regenerate this file again by the tool as manual changes will be lost +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using NFX.Glue; +using NFX.Glue.Protocol; + + +namespace Agni.Social.Graph +{ +// This implementation needs @Agni.@Social.@Graph.@IGraphFriendSystemClient, so +// it can be used with ServiceClientHub class + + /// + /// Client for glued contract Agni.Social.Graph.IGraphFriendSystem server. + /// Each contract method has synchronous and asynchronous versions, the later denoted by 'Async_' prefix. + /// May inject client-level inspectors here like so: + /// client.MsgInspectors.Register( new YOUR_CLIENT_INSPECTOR_TYPE()); + /// + public class GraphFriendSystemClient : ClientEndPoint, @Agni.@Social.@Graph.@IGraphFriendSystemClient + { + + #region Static Members + + private static TypeSpec s_ts_CONTRACT; + private static MethodSpec @s_ms_GetFriendLists_0; + private static MethodSpec @s_ms_AddFriendList_1; + private static MethodSpec @s_ms_DeleteFriendList_2; + private static MethodSpec @s_ms_GetFriendConnections_3; + private static MethodSpec @s_ms_AddFriend_4; + private static MethodSpec @s_ms_AssignFriendLists_5; + private static MethodSpec @s_ms_DeleteFriend_6; + + //static .ctor + static GraphFriendSystemClient() + { + var t = typeof(@Agni.@Social.@Graph.@IGraphFriendSystem); + s_ts_CONTRACT = new TypeSpec(t); + @s_ms_GetFriendLists_0 = new MethodSpec(t.GetMethod("GetFriendLists", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + @s_ms_AddFriendList_1 = new MethodSpec(t.GetMethod("AddFriendList", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@String), typeof(@System.@String) })); + @s_ms_DeleteFriendList_2 = new MethodSpec(t.GetMethod("DeleteFriendList", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@String) })); + @s_ms_GetFriendConnections_3 = new MethodSpec(t.GetMethod("GetFriendConnections", new Type[]{ typeof(@Agni.@Social.@Graph.@FriendQuery) })); + @s_ms_AddFriend_4 = new MethodSpec(t.GetMethod("AddFriend", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@Nullable<@System.@Boolean>) })); + @s_ms_AssignFriendLists_5 = new MethodSpec(t.GetMethod("AssignFriendLists", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@System.@String) })); + @s_ms_DeleteFriend_6 = new MethodSpec(t.GetMethod("DeleteFriend", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID), typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + } + #endregion + + #region .ctor + public GraphFriendSystemClient(string node, Binding binding = null) : base(node, binding) { ctor(); } + public GraphFriendSystemClient(Node node, Binding binding = null) : base(node, binding) { ctor(); } + public GraphFriendSystemClient(IGlue glue, string node, Binding binding = null) : base(glue, node, binding) { ctor(); } + public GraphFriendSystemClient(IGlue glue, Node node, Binding binding = null) : base(glue, node, binding) { ctor(); } + + //common instance .ctor body + private void ctor() + { + + } + + #endregion + + public override Type Contract + { + get { return typeof(@Agni.@Social.@Graph.@IGraphFriendSystem); } + } + + + + #region Contract Methods + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.GetFriendLists'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Collections.@Generic.@IEnumerable<@System.@String>' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Collections.@Generic.@IEnumerable<@System.@String> @GetFriendLists(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var call = Async_GetFriendLists(@gNode); + return call.GetValue<@System.@Collections.@Generic.@IEnumerable<@System.@String>>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.GetFriendLists'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_GetFriendLists(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_GetFriendLists_0, false, RemoteInstance, new object[]{@gNode}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.AddFriendList'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @AddFriendList(@NFX.@DataAccess.@Distributed.@GDID @gNode, @System.@String @list, @System.@String @description) + { + var call = Async_AddFriendList(@gNode, @list, @description); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.AddFriendList'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_AddFriendList(@NFX.@DataAccess.@Distributed.@GDID @gNode, @System.@String @list, @System.@String @description) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_AddFriendList_1, false, RemoteInstance, new object[]{@gNode, @list, @description}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.DeleteFriendList'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @DeleteFriendList(@NFX.@DataAccess.@Distributed.@GDID @gNode, @System.@String @list) + { + var call = Async_DeleteFriendList(@gNode, @list); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.DeleteFriendList'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_DeleteFriendList(@NFX.@DataAccess.@Distributed.@GDID @gNode, @System.@String @list) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_DeleteFriendList_2, false, RemoteInstance, new object[]{@gNode, @list}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.GetFriendConnections'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@FriendConnection>' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@FriendConnection> @GetFriendConnections(@Agni.@Social.@Graph.@FriendQuery @query) + { + var call = Async_GetFriendConnections(@query); + return call.GetValue<@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Graph.@FriendConnection>>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.GetFriendConnections'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_GetFriendConnections(@Agni.@Social.@Graph.@FriendQuery @query) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_GetFriendConnections_3, false, RemoteInstance, new object[]{@query}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.AddFriend'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @AddFriend(@NFX.@DataAccess.@Distributed.@GDID @gNode, @NFX.@DataAccess.@Distributed.@GDID @gFriendNode, @System.@Nullable<@System.@Boolean> @approve) + { + var call = Async_AddFriend(@gNode, @gFriendNode, @approve); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.AddFriend'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_AddFriend(@NFX.@DataAccess.@Distributed.@GDID @gNode, @NFX.@DataAccess.@Distributed.@GDID @gFriendNode, @System.@Nullable<@System.@Boolean> @approve) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_AddFriend_4, false, RemoteInstance, new object[]{@gNode, @gFriendNode, @approve}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.AssignFriendLists'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @AssignFriendLists(@NFX.@DataAccess.@Distributed.@GDID @gNode, @NFX.@DataAccess.@Distributed.@GDID @gFriendNode, @System.@String @lists) + { + var call = Async_AssignFriendLists(@gNode, @gFriendNode, @lists); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.AssignFriendLists'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_AssignFriendLists(@NFX.@DataAccess.@Distributed.@GDID @gNode, @NFX.@DataAccess.@Distributed.@GDID @gFriendNode, @System.@String @lists) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_AssignFriendLists_5, false, RemoteInstance, new object[]{@gNode, @gFriendNode, @lists}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.DeleteFriend'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @DeleteFriend(@NFX.@DataAccess.@Distributed.@GDID @gNode, @NFX.@DataAccess.@Distributed.@GDID @gFriendNode) + { + var call = Async_DeleteFriend(@gNode, @gFriendNode); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphFriendSystem.DeleteFriend'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_DeleteFriend(@NFX.@DataAccess.@Distributed.@GDID @gNode, @NFX.@DataAccess.@Distributed.@GDID @gFriendNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_DeleteFriend_6, false, RemoteInstance, new object[]{@gNode, @gFriendNode}); + return DispatchCall(request); + } + + + #endregion + + }//class +}//namespace diff --git a/src/Agni.Social/Graph/GraphNode.cs b/src/Agni.Social/Graph/GraphNode.cs new file mode 100644 index 0000000..5465f52 --- /dev/null +++ b/src/Agni.Social/Graph/GraphNode.cs @@ -0,0 +1,108 @@ +using NFX.DataAccess.Distributed; +using System; +using Agni.Social.Graph.Server.Data; +using NFX; + +namespace Agni.Social.Graph +{ + /// + /// Contains data about the node of the social graph + /// + [Serializable] + public struct GraphNode + { + + /// + /// Creates a new GraphNode with new GDID + /// + public static GraphNode MakeNew(string nodeType, GDID gOrigShard, GDID gOrig, string origName, byte[] origData, FriendVisibility defaultFriendVisibility) + { + var gdid = NodeRow.GenerateNewNodeRowGDID(); + var utcNow = App.TimeSource.UTCNow; + return new GraphNode(nodeType, gdid, gOrigShard, gOrig, origName, origData, utcNow, defaultFriendVisibility); + } + + public static GraphNode Copy(GraphNode from, + string origName, + byte[] originData, + DateTime utcTimestamp, + FriendVisibility defaultFriendVisibility) + { + return new GraphNode(from.NodeType, from.GDID, from.G_OriginShard, from.G_Origin, origName ?? from.OriginName, originData ?? from.OriginData, utcTimestamp, from.DefaultFriendVisibility); + } + + /// + /// Creates GraphNode, when GDID is assigned the graph system tries to update existing node by GDID, otheriwse + /// creates a new node with the specified GDID. Use MakeNew() to generate new GDID + /// + public GraphNode(string nodeType, + GDID gdid, + GDID gOrigShard, + GDID gOrig, + string origName, + byte[] originData, + DateTime utcTimestamp, + FriendVisibility defaultFriendVisibility) + { + if (nodeType.IsNullOrWhiteSpace() || nodeType.Length>Server.Data.Schema.GSNodeType.MAX_LEN) + throw new GraphException(StringConsts.ARGUMENT_ERROR+"GraphNode.ctor(nodeType=null|>{0})".Args(Server.Data.Schema.GSNodeType.MAX_LEN)); + + NodeType = nodeType; + GDID = gdid; + G_OriginShard = gOrigShard; + G_Origin = gOrig; + OriginName = origName; + OriginData = originData; + TimestampUTC = utcTimestamp; + DefaultFriendVisibility = defaultFriendVisibility; + } + + /// + /// Returns true if this struct is not assigned any value + /// + public bool Unassigned { get{ return NodeType==null;} } + + /// + /// This value depends on the origin database types, i.e.: User, Organization, Forum... + /// + public readonly string NodeType; + + /// + /// Graph Node GDID, unique through different types. + /// The GraphSystem makes this GDID and returns back to the origin for stamping, so + /// original entity may find this GraphNode by this GDID + /// + public readonly GDID GDID; + + /// The sharding GDID in the business origin database, (i.e. G_USER - sharding key) + public readonly GDID G_OriginShard; + + /// + /// The GDID of the the entoty of NodeType in the business origin database, + /// (e.g. G_USERCAR (if the car is kept in user's shard)) - this may or may not be the same as G_OriginShard + /// + public readonly GDID G_Origin; + /// + /// Description, e.g. for user - screen name + /// + public readonly string OriginName; + /// + /// Data about node - BSON + /// + public readonly byte[] OriginData; + /// + /// Creation timestamp + /// + public readonly DateTime TimestampUTC; + /// + /// Determines the default setting of who can see friends of this nodes + /// + public readonly FriendVisibility DefaultFriendVisibility; + + public override string ToString() + { + return "GN({0}, {1}, '{2}')".Args(GDID, NodeType, OriginName); + } + + } +} diff --git a/src/Agni.Social/Graph/GraphNodeSystemClient.cs b/src/Agni.Social/Graph/GraphNodeSystemClient.cs new file mode 100644 index 0000000..2300423 --- /dev/null +++ b/src/Agni.Social/Graph/GraphNodeSystemClient.cs @@ -0,0 +1,214 @@ +//Generated by Agni.Clients.Tools.AgniGluecCompiler + +/* Auto generated by Glue Client Compiler tool (gluec) +on 08.09.2017 18:36:32 at CRAZYROGUE by mad +Do not modify this file by hand if you plan to regenerate this file again by the tool as manual changes will be lost +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using NFX.Glue; +using NFX.Glue.Protocol; + + +namespace Agni.Social.Graph +{ +// This implementation needs @Agni.@Social.@Graph.@IGraphNodeSystemClient, so +// it can be used with ServiceClientHub class + + /// + /// Client for glued contract Agni.Social.Graph.IGraphNodeSystem server. + /// Each contract method has synchronous and asynchronous versions, the later denoted by 'Async_' prefix. + /// May inject client-level inspectors here like so: + /// client.MsgInspectors.Register( new YOUR_CLIENT_INSPECTOR_TYPE()); + /// + public class GraphNodeSystemClient : ClientEndPoint, @Agni.@Social.@Graph.@IGraphNodeSystemClient + { + + #region Static Members + + private static TypeSpec s_ts_CONTRACT; + private static MethodSpec @s_ms_SaveNode_0; + private static MethodSpec @s_ms_GetNode_1; + private static MethodSpec @s_ms_DeleteNode_2; + private static MethodSpec @s_ms_UndeleteNode_3; + private static MethodSpec @s_ms_RemoveNode_4; + + //static .ctor + static GraphNodeSystemClient() + { + var t = typeof(@Agni.@Social.@Graph.@IGraphNodeSystem); + s_ts_CONTRACT = new TypeSpec(t); + @s_ms_SaveNode_0 = new MethodSpec(t.GetMethod("SaveNode", new Type[]{ typeof(@Agni.@Social.@Graph.@GraphNode) })); + @s_ms_GetNode_1 = new MethodSpec(t.GetMethod("GetNode", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + @s_ms_DeleteNode_2 = new MethodSpec(t.GetMethod("DeleteNode", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + @s_ms_UndeleteNode_3 = new MethodSpec(t.GetMethod("UndeleteNode", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + @s_ms_RemoveNode_4 = new MethodSpec(t.GetMethod("RemoveNode", new Type[]{ typeof(@NFX.@DataAccess.@Distributed.@GDID) })); + } + #endregion + + #region .ctor + public GraphNodeSystemClient(string node, Binding binding = null) : base(node, binding) { ctor(); } + public GraphNodeSystemClient(Node node, Binding binding = null) : base(node, binding) { ctor(); } + public GraphNodeSystemClient(IGlue glue, string node, Binding binding = null) : base(glue, node, binding) { ctor(); } + public GraphNodeSystemClient(IGlue glue, Node node, Binding binding = null) : base(glue, node, binding) { ctor(); } + + //common instance .ctor body + private void ctor() + { + + } + + #endregion + + public override Type Contract + { + get { return typeof(@Agni.@Social.@Graph.@IGraphNodeSystem); } + } + + + + #region Contract Methods + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.SaveNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @SaveNode(@Agni.@Social.@Graph.@GraphNode @node) + { + var call = Async_SaveNode(@node); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.SaveNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_SaveNode(@Agni.@Social.@Graph.@GraphNode @node) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_SaveNode_0, false, RemoteInstance, new object[]{@node}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.GetNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphNode' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphNode @GetNode(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var call = Async_GetNode(@gNode); + return call.GetValue<@Agni.@Social.@Graph.@GraphNode>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.GetNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_GetNode(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_GetNode_1, false, RemoteInstance, new object[]{@gNode}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.DeleteNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @DeleteNode(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var call = Async_DeleteNode(@gNode); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.DeleteNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_DeleteNode(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_DeleteNode_2, false, RemoteInstance, new object[]{@gNode}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.UndeleteNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @UndeleteNode(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var call = Async_UndeleteNode(@gNode); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.UndeleteNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_UndeleteNode(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_UndeleteNode_3, false, RemoteInstance, new object[]{@gNode}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.RemoveNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@Agni.@Social.@Graph.@GraphChangeStatus' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @Agni.@Social.@Graph.@GraphChangeStatus @RemoveNode(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var call = Async_RemoveNode(@gNode); + return call.GetValue<@Agni.@Social.@Graph.@GraphChangeStatus>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Graph.IGraphNodeSystem.RemoveNode'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_RemoveNode(@NFX.@DataAccess.@Distributed.@GDID @gNode) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_RemoveNode_4, false, RemoteInstance, new object[]{@gNode}); + return DispatchCall(request); + } + + + #endregion + + }//class +}//namespace diff --git a/src/Agni.Social/Graph/IGraphCommentSystem.cs b/src/Agni.Social/Graph/IGraphCommentSystem.cs new file mode 100644 index 0000000..909be9b --- /dev/null +++ b/src/Agni.Social/Graph/IGraphCommentSystem.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; + + +using NFX.DataAccess.Distributed; +using NFX.Glue; + +using Agni.Contracts; + +namespace Agni.Social.Graph +{ + [Glued] + [LifeCycle(ServerInstanceMode.Singleton)] + public interface IGraphCommentSystem : IAgniService + { + /// + /// Create new comment with rating + /// + /// GDID of autor node + /// GDID of target node + /// Scope for rating + /// Content + /// Byte array of data + /// State of publication + /// star 1-2-3-4-5 + /// Time of current action + /// Comment + Comment Create(GDID gAuthorNode, + GDID gTargetNode, + string dimension, + string content, + byte[] data, + PublicationState publicationState, + RatingValue value = RatingValue.Undefined, + DateTime? timeStamp = null); + + /// + /// Make response to target commentary + /// + /// Author + /// Parent commentary + /// Content of commentary + /// Byte Array + /// New CommentID + Comment Respond(GDID gAuthorNode, CommentID parent, string content, byte[] data); + + /// + /// Updates existing rating by ID + /// + GraphChangeStatus Update(CommentID ratingId, RatingValue value, string content, byte[] data); + + /// + /// Delete comment + /// + /// Existing Comment ID + GraphChangeStatus DeleteComment(CommentID commentId); + + /// + /// Updates likes/dislikes + /// + GraphChangeStatus Like(CommentID commentId, int deltaLike, int deltaDislike); + + /// + /// Check if target node has existing comment, made by author + /// + /// Target node + /// Author + /// Scope of comments + bool IsCommentedByAuthor(GDID gNode, GDID gAuthor, string dimension); + + /// + /// Returns summary ratings per node, dimensions and create date (rating epochs) + /// + IEnumerable GetNodeSummaries(GDID gNode); + + /// + /// Returns comments for Target Node + /// + IEnumerable Fetch(CommentQuery query); + + /// + /// Returns comments for comment by Comment ID + /// + IEnumerable FetchResponses(CommentID commentId); + + /// + /// Returns comment complaints by comment ID + /// + /// + /// + IEnumerable FetchComplaints(CommentID commentId); + + /// + /// Return comment by comment ID + /// + Comment GetComment(CommentID commentId); + + /// + /// Complain about comment + /// + GraphChangeStatus Complain(CommentID commentId, GDID gAuthorNode, string kind, string message); + + /// + /// Justify moderated comment + /// + GraphChangeStatus Justify(CommentID commentID); + } + + public interface IGraphCommentSystemClient : IGraphCommentSystem, IAgniServiceClient + { + //todo Add async versions + } + + public struct CommentQuery : IEquatable + { + //todo design + public const int COMMENT_BLOCK_SIZE = 32; + + public static CommentQuery MakeNew(GDID gTargetNode, string dimension, CommentOrderType orderType, bool asc, DateTime asOfDate, int blockIndex) + { + return new CommentQuery(gTargetNode, dimension, orderType, asc, asOfDate, blockIndex); + } + + internal CommentQuery(GDID gTargetNode, string dimension, CommentOrderType orderType, bool asc, DateTime asOfDate, int blockIndex) + { + G_TargetNode = gTargetNode; + Dimension = dimension; + OrderType = orderType; + Ascending = asc; + AsOfDate = asOfDate; + BlockIndex = blockIndex; + } + + public readonly GDID G_TargetNode; + public readonly string Dimension; + public readonly CommentOrderType OrderType; + public readonly bool Ascending; + + public readonly DateTime AsOfDate; + public readonly int BlockIndex; + + + public bool Equals(CommentQuery other) + { + return this.G_TargetNode == other.G_TargetNode && + this.Dimension == other.Dimension && + this.OrderType == other.OrderType && + this.Ascending == other.Ascending && + this.BlockIndex == other.BlockIndex + ; + } + + public override int GetHashCode() + { + return G_TargetNode.GetHashCode() ^ BlockIndex; + } + + public override bool Equals(object obj) + { + if (! (obj is CommentQuery)) return false; + return Equals((CommentQuery)obj); + } + + } +} diff --git a/src/Agni.Social/Graph/IGraphEventSystem.cs b/src/Agni.Social/Graph/IGraphEventSystem.cs new file mode 100644 index 0000000..ba3d640 --- /dev/null +++ b/src/Agni.Social/Graph/IGraphEventSystem.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using NFX.Glue; +using NFX.DataAccess.Distributed; + +using Agni.Contracts; + +namespace Agni.Social.Graph +{ + /// + /// Handles social graph functionality dealing with event subscription and broadcasting + /// + [Glued] + [LifeCycle(ServerInstanceMode.Singleton)] + public interface IGraphEventSystem : IAgniService + { + /// + /// Emits the event - notifies all subscribers (watchers, friends etc.) about the event. + /// The physical notification happens via IGraphHost implementation + /// + void EmitEvent(Event evt); + + /// + /// Subscribes recipient node to the emitter node. Unlike friends the susbscription connection is uni-directional + /// + void Subscribe(GDID gRecipientNode, GDID gEmitterNode, byte[] parameters); + + /// + /// Removes the subscription. Unlike friends the subscription connection is uni-directional + /// + void Unsubscribe(GDID gRecipientNode, GDID gEmitterNode); + + /// + /// Returns an estimated approximate number of subscribers that an emitter has + /// + long EstimateSubscriberCount(GDID gEmitterNode); + + /// + /// Returns Subscribers for Emitter from start position + /// + IEnumerable GetSubscribers(GDID gEmitterNode, long start, int count); + } + + /// + /// Contract for client of IGraphEventSystem svc + /// + public interface IGraphEventSystemClient : IAgniServiceClient, IGraphEventSystem + { + //todo Add async versions + } +} diff --git a/src/Agni.Social/Graph/IGraphFriendSystem.cs b/src/Agni.Social/Graph/IGraphFriendSystem.cs new file mode 100644 index 0000000..f3dedef --- /dev/null +++ b/src/Agni.Social/Graph/IGraphFriendSystem.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; + +using NFX.Glue; +using NFX.DataAccess.Distributed; + +using Agni.Contracts; + +namespace Agni.Social.Graph +{ + /// + /// Handles the social graph functionality that deals with friend connection, friend list tagging and connection approval + /// + [Glued] + [LifeCycle(ServerInstanceMode.Singleton)] + public interface IGraphFriendSystem : IAgniService + { + /// + /// Returns an enumeration of friend list ids for the particular node + /// + IEnumerable GetFriendLists(GDID gNode); + + /// + /// Adds a new friend list id for the particular node. The list id may not contain commas + /// + GraphChangeStatus AddFriendList(GDID gNode, string list, string description); + + /// + /// Removes friend list id for the particular node + /// + GraphChangeStatus DeleteFriendList(GDID gNode, string list); + + /// + /// Returns an enumeration of FriendConnection{GraphNode, approve date, direction, groups} + /// + IEnumerable GetFriendConnections(FriendQuery query); + + /// + /// Adds a bidirectional friend connection between gNode and gFriendNode + /// If friend connection already exists updates the approve/ban stamp by the receiving party (otherwise approve is ignored) + /// If approve==null then no stamps are set, if true connection is approved given that gNode is not the one who initiated the connection, + /// false then connection is banned given that gNode is not the one who initiated the connection + /// + GraphChangeStatus AddFriend(GDID gNode, GDID gFriendNode, bool? approve); + + + /// + /// Assigns lists to the gNode (the operation is unidirectional - it only assigns the lists on the gNode). + /// Lists is a comma-separated list of friend list ids + /// + GraphChangeStatus AssignFriendLists(GDID gNode, GDID gFriendNode, string lists); + + + /// + /// Deletes friend connections. The operation drops both connections from node and friend + /// + GraphChangeStatus DeleteFriend(GDID gNode, GDID gFriendNode); + } + + + /// + /// Represents query parameters sent to IGraphFriendSystem.GetFriendConnections(query) + /// + public struct FriendQuery + { + public FriendQuery(GDID gNode, FriendStatusFilter status, string orgQry, string lists, int fetchStart, int fetchCount) + { + G_Node = gNode; + Status = status; + OriginQuery = orgQry; + Lists = lists; + FetchStart = fetchStart; + FetchCount = fetchCount; + } + + /// Node for which friends are returned + public readonly GDID G_Node; + + public readonly FriendStatusFilter Status; + + /// Pass expression with * to search by name + public readonly string OriginQuery; + + /// A comma-delimited list of friend list ids, null = all + public readonly string Lists; + + /// From what position to start fetching + public readonly int FetchStart; + + /// How many records to fetch + public readonly int FetchCount; + } + + /// + /// Contract for client of IGraphFriendSystem svc + /// + public interface IGraphFriendSystemClient : IAgniServiceClient, IGraphFriendSystem + { + //todo Add async versions + } +} diff --git a/src/Agni.Social/Graph/IGraphNodeSystem.cs b/src/Agni.Social/Graph/IGraphNodeSystem.cs new file mode 100644 index 0000000..8cfd81c --- /dev/null +++ b/src/Agni.Social/Graph/IGraphNodeSystem.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +using NFX.Glue; +using NFX.DataAccess.Distributed; + +using Agni.Contracts; + +namespace Agni.Social.Graph +{ + /// + /// Handles the base social graph functionality such as CRUD of graph nodes (users, forums, groups etc..) + /// + [Glued] + public interface IGraphNodeSystem : IAgniService + { + /// + /// Saves the GraphNode instances into the system. + /// If a node with such ID already exists, updates it, otherwise creates a new node + /// Return GDID Node + /// + GraphChangeStatus SaveNode(GraphNode node); + + /// + /// Fetches the GraphNode by its unique GDID or unassigned node if not found + /// + GraphNode GetNode(GDID gNode); + + /// + /// Deletes node by GDID + /// + GraphChangeStatus DeleteNode(GDID gNode); + + /// + /// Undeletes node by GDID + /// + GraphChangeStatus UndeleteNode(GDID gNode); + + /// + /// Physically removes node by GDID from database + /// + GraphChangeStatus RemoveNode(GDID gNode); + } + + /// + /// Contract for client of IGraphSystem svc + /// + public interface IGraphNodeSystemClient : IAgniServiceClient, IGraphNodeSystem + { + //todo Add async versions + } +} diff --git a/src/Agni.Social/Graph/Server/Data/BaseRows.cs b/src/Agni.Social/Graph/Server/Data/BaseRows.cs new file mode 100644 index 0000000..9e124dc --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/BaseRows.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; + + +namespace Agni.Social.Graph.Server.Data +{ + public abstract class BaseRow : AmorphousTypedRow + { + public BaseRow() : base() + { + + } + } + + /// + /// Base class for all GraphRows + /// + public abstract class BaseRowWithGDID : BaseRow + { + public static UniqueSequenceAttribute GetGDIDAttribute(Type tRow) + { + if (!typeof(BaseRow).IsAssignableFrom(tRow)) + throw new GraphException("{0}: {1} is not BaseRow".Args("GetGDIDAttribute", tRow.FullName)); + + var attr = UniqueSequenceAttribute.GetForRowType(tRow); + if (attr == null || + attr.Scope.IsNullOrWhiteSpace() || + attr.Sequence.IsNullOrWhiteSpace()) + throw new GraphException("Both scope and sequence must be defined in UniqueSequenceAttribute decorating {0}".Args(tRow.FullName)); + + return attr; + } + + public BaseRowWithGDID() : base() + { + + } + + public BaseRowWithGDID(bool newGdid) : base() + { + if (newGdid) + { + GDID = GenerateNewGDID(); + } + } + + /// + /// Generates new gdid properly scoping it and naming it for this row (see GDIDSequenceName) + /// + public static GDID GenerateNewGDID(Type t) + { + var attr = GetGDIDAttribute(t); + return GraphOperationContext.Instance.DataStore.GDIDGenerator.GenerateOneGDID(attr.Scope, attr.Sequence); + } + + /// + /// Generates new gdid properly scoping it and naming it for this row (see GDIDSequenceName) + /// + public GDID GenerateNewGDID() + { + return BaseRowWithGDID.GenerateNewGDID(this.GetType()); + } + + /// + /// Primary KEY + /// + [Field(targetName: TargetedAttribute.ANY_TARGET, required: true, key: true, visible: false)] + public GDID GDID { get ; set; } + } +} diff --git a/src/Agni.Social/Graph/Server/Data/CommentRow.cs b/src/Agni.Social/Graph/Server/Data/CommentRow.cs new file mode 100644 index 0000000..773a9dc --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/CommentRow.cs @@ -0,0 +1,93 @@ +using System; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using Agni.Social.Graph.Server.Data.Schema; + +namespace Agni.Social.Graph.Server.Data +{ + /// + /// Comment data stored in COMMENT area. Every comment has a unique ID, but is briefcased by G_COMMENTVOLUME + /// + [Table(name: "tbl_comment")] + [UniqueSequence(SocialConsts.MDB_AREA_COMMENT, "comment")] + public sealed class CommentRow : BaseRowWithGDID + { + public CommentRow() : base() + { + } + + public CommentRow(bool newGdid) : base(newGdid) + { + } + + /// + /// Briefcase key + /// + [Field(backendName: "G_VOL", required: true)] + public GDID G_CommentVolume { get; set; } + + [Field(backendName: "DIM", required: true)] + public string Dimension { get; set; } + + [Field(backendName: "G_ATR", required: true)] + public GDID G_AuthorNode { get; set; } + + [Field(backendName: "G_TRG", required: true)] + public GDID G_TargetNode { get; set; } + + [Field(backendName: "ROOT", required: true)] + public bool IsRoot { get; set; } + + [Field(backendName: "G_PAR")] + public GDID? G_Parent { get; set; } + + [Field(backendName: "MSG", + minLength: GSCommentMessage.MIN_LEN, + maxLength: GSCommentMessage.MAX_LEN, + required: true)] + public string Message { get; set; } + + [Field(backendName: "DAT", required: true)] + public byte[] Data { get; set; } + + [Field(backendName: "CDT", required: true)] + public DateTime Create_Date { get; set; } + + [Field(backendName: "LKE", required: true)] + public uint Like { get;set; } + + [Field(backendName: "DIS", required: true)] + public uint Dislike { get; set; } + + [Field(backendName: "CMP", required: true)] + public uint ComplaintCount { get; set; } + + [Field(backendName: "PST", required: true)] + public string PublicationState { get; set; } + + [Field(backendName: "RTG", required: true)] + public byte Rating { get; set; } + + [Field(backendName: "RCNT", required: true)] + public uint ResponseCount { get; set; } + + [Field(required: true)] + public bool In_Use { get ; set; } + + /// + /// Update count of commentaries + /// + /// +/- Delta + public void UpdateResponseCount(int delta) + { + var isNegative = delta < 0; + if (isNegative && ResponseCount == 0) + return; + + var abs = (uint)Math.Abs(delta); + ResponseCount = isNegative + ? ResponseCount - abs + : ResponseCount + abs; + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/CommentVolumeRow.cs b/src/Agni.Social/Graph/Server/Data/CommentVolumeRow.cs new file mode 100644 index 0000000..32cec65 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/CommentVolumeRow.cs @@ -0,0 +1,32 @@ +using System; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph.Server.Data +{ + [Table(name: "tbl_commentvol")] + public sealed class CommentVolumeRow : BaseRow + { + /// Owner (target of comment such as PRODUCT NODE) GDID; Briefcase key for this row + [Field(backendName: "G_OWN", required: true, key: true)] + public GDID G_Owner { get; set; } + + /// Link for Comment Volume; brifcase key for COMMENT; obtained from NODE sequence + [Field(backendName: "G_VOL", required: true, key: true)] + public GDID G_CommentVolume { get; set; } + + /// + /// Rating dimensional + /// + [Field(backendName: "DIM", required: true)] + public string Dimension { get; set; } + + /// Count message in volume + [Field(backendName: "CNT", required: true)] + public int Count { get; set; } + + /// Create date volume + [Field(backendName: "CDT", required: true)] + public DateTime Create_Date { get; set; } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/ComplaintRow.cs b/src/Agni.Social/Graph/Server/Data/ComplaintRow.cs new file mode 100644 index 0000000..fb366c2 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/ComplaintRow.cs @@ -0,0 +1,42 @@ +using System; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using Agni.Social.Graph.Server.Data.Schema; + +namespace Agni.Social.Graph.Server.Data +{ + /// + /// Comment complaint data stored in COMMENT area + /// + [Table(name: "tbl_complaint")] + [UniqueSequence(SocialConsts.MDB_AREA_COMMENT, "complaint")] + public sealed class ComplaintRow : BaseRowWithGDID + { + public ComplaintRow() : base() {} + public ComplaintRow(bool newGdid) : base(newGdid) {} + + [Field(backendName: "G_CMT", required: true)] + public GDID G_Comment { get; set; } + + [Field(backendName: "G_ATH", required: true)] + public GDID G_AuthorNode { get; set; } + + [Field(backendName: "KND", + required: true, + min: GSMessageType.MIN_LEN, + max: GSMessageType.MAX_LEN)] + public string Kind { get; set; } + + [Field(backendName: "MSG", + minLength: GSCommentMessage.MIN_LEN, + maxLength: GSCommentMessage.MAX_LEN, + required: false)] + public string Message { get; set; } + + [Field(backendName: "CDT", required: true)] + public DateTime Create_Date { get; set; } + + [Field(required: true)] + public bool In_Use { get ; set; } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/FriendListRow.cs b/src/Agni.Social/Graph/Server/Data/FriendListRow.cs new file mode 100644 index 0000000..1164964 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/FriendListRow.cs @@ -0,0 +1,45 @@ +using System; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; + +using Agni.Social.Graph.Server.Data.Schema; + +namespace Agni.Social.Graph.Server.Data +{ + /// + /// A list of named friend lists - a named group of friends like "family", "coworkers" etc. + /// + [Table(name: "tbl_friendlist")] + [UniqueSequence(SocialConsts.MDB_AREA_NODE, "flist")] + public sealed class FriendListRow : BaseRowWithGDID + { + + public FriendListRow() : base() + { + + } + + public FriendListRow(bool gdid) : base(gdid) + { + + } + + /// Node tha this list belongs to - brief case key + [Field(backendName: "G_OWN", required: true)] + public GDID G_Owner { get; set; } + + /// List ID + [Field(backendName: "LID", required: true, minLength: GSFriendListID.MIN_LEN, maxLength: GSFriendListID.MAX_LEN)] + public string List_ID { get; set; } + + /// List description + [Field(backendName: "LDR", required: false, minLength: GSFriendListID.MIN_LEN, maxLength: GSFriendListID.MAX_LEN)] + public string List_Description { get; set; } + + /// When created + [Field(backendName: "CDT", required: true)] + public DateTime? Create_Date { get; set; } + } +} diff --git a/src/Agni.Social/Graph/Server/Data/FriendRow.cs b/src/Agni.Social/Graph/Server/Data/FriendRow.cs new file mode 100644 index 0000000..b4144a4 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/FriendRow.cs @@ -0,0 +1,62 @@ +using System; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; + +using Agni.Social.Graph.Server.Data.Schema; + +namespace Agni.Social.Graph.Server.Data +{ + /// + /// GraphNode data + /// + [Table(name: "tbl_friend")] + [UniqueSequence(SocialConsts.MDB_AREA_NODE, "friend")] + public sealed class FriendRow : BaseRowWithGDID + { + + public FriendRow() : base() + { + + } + + public FriendRow(bool newGdid) : base(newGdid) + { + + } + + /// Owner GDID; Briefcase key + [Field(backendName: "G_OWN", required: false)] + public GDID G_Owner { get; set; } + + /// Pointer to a friend + [Field(backendName: "G_FND", required: true)] + public GDID G_Friend { get; set; } + + /// When connection formed + [Field(backendName: "G_RDT", required: true)] + public DateTime Request_Date { get; set; } + + /// If set, then approved + [Field(backendName: "G_SDT", required: true)] + public DateTime Status_Date { get; set; } + + /// Approval status + [Field(backendName: "STS", required: true, valueList: GSFriendStatus.VALUE_LIST)] + public string Status { get; set; } + + /// Who requested friendship + [Field(backendName: "DIR", required: true, valueList: GSFriendshipRequestDirection.VALUE_LIST)] + public string Direction { get; set; } + + /// Who can see this connection - cascades from user profile + [Field(backendName: "VIS", required: true, valueList: GSFriendVisibility.VALUE_LIST)] + public string Visibility { get; set; } + + /// Comma-separated list of friend list Ids + [Field(backendName: "LST", required: false)] + public string Lists { get; set; } + + } +} diff --git a/src/Agni.Social/Graph/Server/Data/NodeRatingRow.cs b/src/Agni.Social/Graph/Server/Data/NodeRatingRow.cs new file mode 100644 index 0000000..dbed2a0 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/NodeRatingRow.cs @@ -0,0 +1,169 @@ +using System; + +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX; + +namespace Agni.Social.Graph.Server.Data +{ + /// + /// The table contains data for nodes that have rating. 1:M per dimension to Node extension + /// + [Table(name: "tbl_noderating")] + public sealed class NodeRatingRow : BaseRow + { + /// + /// Primary key GDID + /// + [Field(backendName: "G_NOD", required: true, key: true)] + public GDID G_Node { get; set; } + + /// + /// Primary key, Dimension of rating, e.g. if we rate cars, the dims are 'reliability', 'driving" etc. + /// + [Field(backendName: "DIM", required: true, key: true)] + public string Dimension { get; set; } + + /// + /// Also known as epoch + /// + [Field(backendName: "CDT", required: true)] + public DateTime Create_Date { get; set; } + + /// + /// When was the node's rating last changed + /// + [Field(backendName: "LCD", required: true)] + public DateTime Last_Change_Date { get; set; } + + /// + /// Count of commetaries + /// + [Field(backendName: "CNT", required: true)] + public ulong Cnt { get; set; } + + /// + /// Rating star 1 count + /// + [Field(backendName: "RTG1", required: true)] + public ulong Rating1 { get; set; } + + /// + /// Rating star 2 count + /// + [Field(backendName: "RTG2", required: true)] + public ulong Rating2 { get; set; } + + /// + /// Rating star 3 count + /// + [Field(backendName: "RTG3", required: true)] + public ulong Rating3 { get; set; } + + /// + /// Rating star 4 count + /// + [Field(backendName: "RTG4", required: true)] + public ulong Rating4 { get; set; } + + /// + /// Rating star 5 count + /// + [Field(backendName: "RTG5", required: true)] + public ulong Rating5 { get; set; } + + /// + /// Total amount of ratings + /// + public ulong TotalRatings + { + get { return Rating1 + Rating2 + Rating3 + Rating4 + Rating5; } + } + + /// + /// Average rating + /// + public float Rating + { + get + { + var tr = (float) TotalRatings; + return tr > 0f + ? ((Rating1*1) + + (Rating2*2) + + (Rating3*3) + + (Rating4*4) + + (Rating5*5) + )/tr + : 0f; + } + } + + /// + /// Decrease or increase rating by delta + /// + /// Star 1-2-3-4-5 + /// +/- Delta + public void UpdateRating(RatingValue value, int delta) + { + var isNegative = delta < 0; + var abs = (ulong) Math.Abs(delta); + switch (value) + { + // if isNegative == true and rating counter == 0, decreasing ulong will make field MAX ulong. + // so, need to check it + case RatingValue.Star1: + if (isNegative && Rating1 == 0) + break; + Rating1 = isNegative + ? Rating1 - abs + : Rating1 + abs; + break; + case RatingValue.Star2: + if (isNegative && Rating2 == 0) + break; + Rating2 = isNegative + ? Rating2 - abs + : Rating2 + abs; + break; + case RatingValue.Star3: + if (isNegative && Rating3 == 0) + break; + Rating3 = isNegative + ? Rating3 - abs + : Rating3 + abs; + break; + case RatingValue.Star4: + if (isNegative && Rating4 == 0) + break; + Rating4 = isNegative + ? Rating4 - abs + : Rating4 + abs; + break; + case RatingValue.Star5: + if (isNegative && Rating5 == 0) + break; + Rating5 = isNegative + ? Rating5 - abs + : Rating5 + abs; + break; + } + } + + /// + /// Update count of commentaries + /// + /// +/- Delta + public void UpdateCount(int delta) + { + var isNegative = delta < 0; + if (isNegative && Cnt == 0) + return; + + var abs = (ulong) Math.Abs(delta); + Cnt = isNegative + ? Cnt - abs + : Cnt + abs; + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/NodeRow.cs b/src/Agni.Social/Graph/Server/Data/NodeRow.cs new file mode 100644 index 0000000..356aa53 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/NodeRow.cs @@ -0,0 +1,66 @@ +using System; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; + +using Agni.Social.Graph.Server.Data.Schema; + +namespace Agni.Social.Graph.Server.Data +{ + /// + /// GraphNode data + /// + [Table(name: "tbl_node")] + [UniqueSequence(SocialConsts.MDB_AREA_NODE, "node")] + public sealed class NodeRow : BaseRowWithGDID + { + public static GDID GenerateNewNodeRowGDID() + { + return GenerateNewGDID(typeof(NodeRow)); + } + + public NodeRow() : base() + { + + } + + public NodeRow(bool newGdid) : base(newGdid) + { + } + + [Field(backendName: "TYP", required: true, minLength: GSNodeType.MIN_LEN, maxLength: GSNodeType.MAX_LEN)] + public string Node_Type { get; set; } + + /// + /// Stores GDID of origin shard + /// + [Field(backendName: "G_OSH", required: true)] + public GDID G_OriginShard { get; set; } + + /// + /// Stores the GDID of the origin entity + /// + [Field(backendName: "G_ORI", required: true)] + public GDID G_Origin { get; set; } + + /// + /// Not required for subscriber link + /// + [Field(backendName: "ONM", required: true)] + public string Origin_Name { get; set; } + + [Field(backendName: "ODT", required: false)] + public byte[] Origin_Data { get; set; } + + [Field(backendName: "CDT", required: true)] + public DateTime? Create_Date { get; set; } + + /// Default Friend Visibility + [Field(backendName: "FVI", required: false, valueList: GSFriendVisibility.VALUE_LIST)] + public string Friend_Visibility { get; set; } + + [Field(required: true)] + public bool In_Use { get ; set; } + } +} diff --git a/src/Agni.Social/Graph/Server/Data/Queries.cs b/src/Agni.Social/Graph/Server/Data/Queries.cs new file mode 100644 index 0000000..a643223 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Queries.cs @@ -0,0 +1,379 @@ +using System; +using Agni.Social.Graph.Server.Data.Schema; +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Wave.Instrumentation; + +namespace Agni.Social.Graph.Server.Data +{ + internal static class Queries + { + + #region Nodes and Subscribers + + public static Query FindOneNodeByGDID(GDID gNode) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Node.FindOneNodeByGDID") + { + new Query.Param("pgnode", gNode) + }; + } + + public static Query ChangeInUseNodeByGDID(GDID gNode, bool isDel) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Node.DeleteOneNodeByGDID") + { + new Query.Param("pgnode", gNode), + new Query.Param("pInUse", isDel ? "F" : "T") + }; + } + + public static Query CountSubscribers(GDID gNode) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Node.CountSubscribers") + { + new Query.Param("pNode",gNode) + }; + } + + public static Query FindSubscriber(SubscriberVolumeRow volume, GDID gSubscriber) where TRow:Row + { + return new Query("Graph.Server.Data.Scripts.Node.FindSubscriber") + { + new Query.Param("pVol",volume), + new Query.Param("pSub",gSubscriber) + }; + } + + public static Query FindSubscriberVolumes(GDID gNode) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Node.FindSubscriberVolumes") + { + new Query.Param("pNode",gNode) + }; + } + + public static Query CountSubscriberVolumes(GDID gNode) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Node.CountSubscriberVolumes") + { + new Query.Param("pNode",gNode) + }; + } + + public static Query FindSubscribers(GDID gVolume, long start, int count) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Node.FindSubscribers") + { + new Query.Param("pVol",gVolume), + new Query.Param("pStart",start), + new Query.Param("pCount",count) + }; + } + + public static Query GetNextVolume(GDID gNode, int start) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Node.GetNextVolume") + { + new Query.Param("pNode", gNode), + new Query.Param("pStart", start) + }; + } + + public static Query RemoveNode(GDID gNode) + { + return new Query("Graph.Server.Data.Scripts.Node.RemoveNode") + { + new Query.Param("pNode", gNode) + }; + } + + public static Query RemoveSubVol(GDID gNode) + { + return new Query("Graph.Server.Data.Scripts.Node.RemoveSubVol") + { + new Query.Param("pNode", gNode) + }; + } + + public static Query RemoveSubscribers(GDID gVol) + { + return new Query("Graph.Server.Data.Scripts.Node.RemoveSubscribers") + { + new Query.Param("pVol", gVol) + }; + } + + #endregion + + #region Friend + + public static Query FindOneFriendByNodeAndFriend(GDID gNode, GDID gFriendNode) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Friend.FindOneFriendByNodeAndFriend") + { + new Query.Param("pown", gNode), + new Query.Param("pfnd", gFriendNode) + }; + } + + public static Query FindFriends(FriendQuery query) where TRow : Row + { + var status = "%"; + switch (query.Status) + { + case FriendStatusFilter.Approved: + status = GSFriendStatus.APPROVED; + break; + case FriendStatusFilter.Banned: + status = GSFriendStatus.BANNED; + break; + case FriendStatusFilter.PendingApproval: + status = GSFriendStatus.PENDING; + break; + } + + return new Query("Graph.Server.Data.Scripts.Friend.FindFriends") + { + new Query.Param("pNode", query.G_Node), + new Query.Param("pList", query.Lists), + new Query.Param("pStatus", status), + new Query.Param("pFetchStart", query.FetchStart), + new Query.Param("pFetchCount", query.FetchCount) + }; + } + + public static Query CountFriends(GDID gNode) where TRow:Row + { + return new Query("Graph.Server.Data.Scripts.Friend.CountFriends") + { + new Query.Param("pG_Node",gNode) + }; + } + + public static Query FindFriendListByNode(GDID gNode) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Friend.FindFriendListByNode") + { + new Query.Param("pgnode", gNode) + }; + } + + public static Query FindAllFriends(GDID gNode) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.FindAllFriends") + { + new Query.Param("pNode", gNode) + }; + } + + public static Query DeleteFriendByNode(GDID gNode) + { + return new Query("Graph.Server.Data.Scripts.Friend.DeleteFriendByNode") + { + new Query.Param("gG_Node", gNode) + }; + } + + public static Query DeleteFriendByNodeAndFriend(GDID gNode, GDID gFriendNode) + { + return new Query("Graph.Server.Data.Scripts.Friend.DeleteFriendByNodeAndFriend") + { + new Query.Param("pown", gNode), + new Query.Param("pfnd", gFriendNode), + new Query.Param("pdt", App.TimeSource.UTCNow) + }; + } + + public static Query DeleteFriendListByListId(GDID gNode, string list) + { + return new Query("Graph.Server.Data.Scripts.Friend.DeleteFriendListByListId") + { + new Query.Param("pgnode", gNode), + new Query.Param("plistid", list) + }; + } + + public static Query RemoveFriendByNode(GDID gNode) + { + return new Query("Graph.Server.Data.Scripts.Friend.RemoveFriendByNode") + { + new Query.Param("pNode", gNode) + }; + } + + public static Query RemoveFriendListByNode(GDID gNode) + { + return new Query("Graph.Server.Data.Scripts.Friend.RemoveFriendListByNode") + { + new Query.Param("pNode", gNode) + }; + } + + public static Query GetNextFriend(GDID gNode, int start) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Friend.GetNextFriend") + { + new Query.Param("pNode", gNode), + new Query.Param("pStart", start) + }; + } + + #endregion + + #region Comments and ratings + + public static Query FindNodeRating(GDID gNode, string dimension) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindNodeRating") + { + new Query.Param("pNode", gNode), + new Query.Param("pDim", dimension) + }; + } + + public static Query FindNodeRatings(GDID gNode, DateTime dt) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindNodeRatings") + { + new Query.Param("pNode", gNode), + new Query.Param("pDT", dt) + }; + } + + public static Query ClearNodeRating(GDID gNode, string dimension, DateTime utc) + { + return new Query("Graph.Server.Data.Scripts.Rating.ClearNodeRating") + { + new Query.Param("pNode", gNode), + new Query.Param("pDim", dimension), + new Query.Param("pCDT", utc) + }; + } + + public static Query ClearNodeRatings(GDID gNode, DateTime utc) + { + return new Query("Graph.Server.Data.Scripts.Rating.ClearNodeRatings") + { + new Query.Param("pNode", gNode), + new Query.Param("pCDT", utc) + }; + } + + public static Query UpdateLike(GDID gVolume, GDID gComment, int deltaLike, int deltaDislike) + { + return new Query("Graph.Server.Data.Scripts.Rating.UpdateLike") + { + new Query.Param("pVolume", gVolume), + new Query.Param("pComment", gComment), + new Query.Param("pdtLike", deltaLike), + new Query.Param("pdtDislike", deltaDislike) + }; + } + + public static Query CancelComplaintsByComment(GDID gComment) + { + return new Query("Graph.Server.Data.Scripts.Rating.CancelComplaintsByComment") + { + new Query.Param("pG_Comment", gComment) + }; + } + + public static Query FindCommentVolume(GDID gNode, GDID gVolume) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindCommentVolume") + { + new Query.Param("pNode", gNode), + new Query.Param("pVol", gVolume) + }; + } + + public static Query FindEmptyCommentVolume(GDID gNode, string dimension, int maxCount) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindEmptyCommentVolume") + { + new Query.Param("pNode", gNode), + new Query.Param("pDim", dimension), + new Query.Param("pMaxCount", maxCount) + }; + } + + public static Query FindCommentByGDID(GDID gComment) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindCommentByGDID") + { + new Query.Param("pGDID", gComment) + }; + } + + public static Query FindComments(GDID gNode, string dimension, bool isRoot) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindComments") + { + new Query.Param("pNode", gNode), + new Query.Param("pDim", dimension), + new Query.Param("pRoot", isRoot) + }; + } + + public static Query HasCommentsCreatedByAuthor(GDID gNode, GDID gAuthor, string dimension) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Rating.HasCommentsCreatedByAuthor") + { + new Query.Param("pNode", gNode), + new Query.Param("pAuthor", gAuthor), + new Query.Param("pDim", dimension) + }; + } + + public static Query DeleteResponses(CommentID commentId) + { + return new Query("Graph.Server.Data.Scripts.Rating.DeleteResponses") + { + new Query.Param("pParent", commentId.G_Comment), + new Query.Param("pVolume", commentId.G_Volume) + }; + } + + public static Query CountResponses(CommentID commentId) where TRow: Row + { + return new Query("Graph.Server.Data.Scripts.Rating.CountResponses") + { + new Query.Param("pComment", commentId.G_Comment), + new Query.Param("pVolume", commentId.G_Volume) + }; + } + + public static Query FindResponses(CommentID commentId) where TRow:Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindResponses") + { + new Query.Param("pComment", commentId.G_Comment), + new Query.Param("pVolume", commentId.G_Volume) + }; + } + + public static Query FindComplaints(GDID gComment) where TRow : Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindComplaints") + { + new Query.Param("pComment", gComment) + }; + } + + public static Query FindCommentVolumes(GDID gNode, string dimension,int count, DateTime cdt) where TRow: Row + { + return new Query("Graph.Server.Data.Scripts.Rating.FindCommentVolumes") + { + new Query.Param("pNode", gNode), + new Query.Param("pDIM", dimension), + new Query.Param("pCDT", cdt), + new Query.Param("cnt", count) + }; + } + + #endregion + + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Schema/Domains.cs b/src/Agni.Social/Graph/Server/Data/Schema/Domains.cs new file mode 100644 index 0000000..935553f --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Schema/Domains.cs @@ -0,0 +1,394 @@ +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using NFX; +using NFX.RelationalModel; +using NFX.RelationalModel.DataTypes; + +namespace Agni.Social.Graph.Server.Data.Schema +{ + public abstract class GSDomain : RDBMSDomain + { + protected GSDomain() : base() { } + } + + + public abstract class GSEnum : GSDomain + { + public DBCharType Type; + + public int Size; + + public string[] Values; + + protected GSEnum(DBCharType type, string values) + { + Type = type; + var vlist = values.Split('|'); + Size = vlist.Max(v => v.Trim().Length); + if (Size < 1) Size = 1; + Values = vlist; + } + + public override string GetTypeName(RDBMSCompiler compiler) + { + return Type == DBCharType.Varchar ? "VARCHAR({0})".Args(Size) : "CHAR({0})".Args(Size); + } + + public override string GetColumnCheckScript(RDBMSCompiler compiler, RDBMSEntity column, Compiler.Outputs outputs) + { + var enumLine = string.Join(", ", Values.Select(v => compiler.EscapeString(v.Trim()))); + return compiler.TransformKeywordCase("check ({0} in ({1}))") + .Args( + compiler.GetQuotedIdentifierName(RDBMSEntityType.Column, column.TransformedName), + enumLine + ); + } + } + + + public abstract class GSGDID : GSDomain + { + public readonly bool Required; + + public GSGDID(bool required) + { + Required = required; + } + + public override bool? GetColumnRequirement(RDBMSCompiler compiler) + { + return Required; + } + + public override string GetTypeName(RDBMSCompiler compiler) + { + return Required ? "BINARY(12)" : "VARBINARY(12)"; + } + } + + + public abstract class GSGDIDRef : GSGDID + { + public GSGDIDRef(bool required) : base(required) { } + + public override void TransformColumnName(RDBMSCompiler compiler, RDBMSEntity column) + { + column.TransformedName = "G_{0}".Args(column.TransformedName); + } + } + + + public sealed class GSRequiredGDID : GSGDID { public GSRequiredGDID() : base(true) { } } + public sealed class GSNullableGDID : GSGDID { public GSNullableGDID() : base(false) { } } + public sealed class GSRequiredGDIDRef : GSGDIDRef { public GSRequiredGDIDRef() : base(true) { } } + public sealed class GSNullableGDIDRef : GSGDIDRef { public GSNullableGDIDRef() : base(false) { } } + + + public sealed class GSBool : GSEnum { public const int MAX_LEN = 1; public GSBool() : base(DBCharType.Char, "T|F") { } } + + public sealed class GSDescription : GSDomain + { + public const int MAX_LEN = 64; + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "varchar({0})".Args(MAX_LEN); + } + } + + + public sealed class GSNodeName : GSDomain + { + public const int MAX_LEN = 48; + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "varchar({0})".Args(MAX_LEN); + } + } + + + /// + /// Node type such as: User, Room, Forum, Group etc. + /// + public sealed class GSNodeType : GSDomain + { + public const int MIN_LEN = 1; + public const int MAX_LEN = 8; // So we can pack in ulong + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "char({0})".Args(MAX_LEN); + } + } + + /// Such as Subscription parameters + public sealed class GSParameters : GSDomain + { + public const int MIN_LEN = 3; + public const int MAX_LEN = 1024; + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "varchar({0})".Args(MAX_LEN); + } + } + + + /// + /// No second fraction resolution + /// + public sealed class GSTimestamp : GSDomain + { + public override string GetTypeName(RDBMSCompiler compiler) + { + return "datetime(0)"; + } + } + + public sealed class GSFriendListID : GSDomain + { + public const int MIN_LEN = 1; + public const int MAX_LEN = 16; + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "varchar({0})".Args(MAX_LEN); + } + } + + public sealed class GSFriendListIDs : GSDomain + { + public const int MIN_LEN = 1; + public const int MAX_LEN = (GSFriendListID.MAX_LEN+1/*for comma*/)*12;//12 lists max + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "varchar({0})".Args(MAX_LEN); + } + } + + public sealed class GSFriendStatus : GSEnum + { + public const int MAX_LEN = 1; + + public const string PENDING = "P"; + public const string APPROVED = "A"; + public const string DENIED = "D"; + public const string BANNED = "B"; + + public const string VALUE_LIST = "P: Pending, A: Approved, D: Denied, B: Banned"; + + public static string ToDomainString(FriendStatus status) + { + switch (status) + { + case FriendStatus.Approved: return GSFriendStatus.APPROVED; + case FriendStatus.Denied: return GSFriendStatus.DENIED; + case FriendStatus.Banned: return GSFriendStatus.BANNED; + default: return GSFriendStatus.PENDING; + } + } + + public static FriendStatus ToFriendStatus(string status) + { + if (status.EqualsOrdIgnoreCase(GSFriendStatus.APPROVED))return FriendStatus.Approved; + if (status.EqualsOrdIgnoreCase(GSFriendStatus.DENIED)) return FriendStatus.Denied; + if (status.EqualsOrdIgnoreCase(GSFriendStatus.BANNED)) return FriendStatus.Banned; + + return FriendStatus.Pending; + } + + public GSFriendStatus() : base(DBCharType.Char, "P|A|D|B") { } + } + + + public sealed class GSFriendshipRequestDirection : GSEnum + { + public const int MAX_LEN = 1; + + public const string I = "I"; + public const string FRIEND = "F"; + + public const string VALUE_LIST = "I: I myself, F: Friend"; + + public static string ToDomainString(FriendshipRequestDirection dir) + { + if (dir==FriendshipRequestDirection.Friend) return FRIEND; + return I; + } + + public static FriendshipRequestDirection ToFriendshipRequestDirection(string status) + { + if (status.EqualsOrdIgnoreCase(GSFriendshipRequestDirection.FRIEND))return FriendshipRequestDirection.Friend; + + return FriendshipRequestDirection.I; + } + + public GSFriendshipRequestDirection() : base(DBCharType.Char, "I|F") { } + } + + + public sealed class GSFriendVisibility : GSEnum + { + public const int MAX_LEN = 1; + + public const string ANYONE = "A"; + public const string PUBLIC = "P"; + public const string FRIENDS = "F"; + public const string PRIVATE = "T"; + + public const string VALUE_LIST = "A: Anyone, P: Public, F: Friends, T: Private"; + + public static string ToDomainString(FriendVisibility vis) + { + switch(vis) + { + case FriendVisibility.Anyone: return GSFriendVisibility.ANYONE; + case FriendVisibility.Friends: return GSFriendVisibility.FRIENDS; + case FriendVisibility.Public: return GSFriendVisibility.PUBLIC; + default: return GSFriendVisibility.PRIVATE; + } + } + + public static FriendVisibility ToFriendVisibility(string status) + { + if (status.EqualsOrdIgnoreCase(GSFriendVisibility.ANYONE)) return FriendVisibility.Anyone; + if (status.EqualsOrdIgnoreCase(GSFriendVisibility.PUBLIC)) return FriendVisibility.Public; + if (status.EqualsOrdIgnoreCase(GSFriendVisibility.FRIENDS))return FriendVisibility.Friends; + + return FriendVisibility.Private; + } + + public GSFriendVisibility() : base(DBCharType.Char, "A|P|F|T") { } + } + + + public sealed class GSBSONData : GSDomain + { + public const int MAX_LEN = 256; + + public readonly bool Required; + + public GSBSONData() + { + Required = false; + } + + public GSBSONData(bool required) + { + Required = required; + } + + public override bool? GetColumnRequirement(RDBMSCompiler compiler) + { + return Required; + } + public override string GetTypeName(RDBMSCompiler compiler) + { + return Required ? "BINARY({0})".Args(MAX_LEN) : "VARBINARY({0})".Args(MAX_LEN); + } + } + + public class GSCounter : GSDomain + { + public GSCounter() : base() { } + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "BIGINT(8) UNSIGNED"; + } + } + + public sealed class GSMessageType : GSDomain + { + public const int MIN_LEN = 1; + public const int MAX_LEN = 8; // So we can pack in ulong + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "char({0})".Args(MAX_LEN); + } + } + + public sealed class GSRating : GSDomain + { + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "tinyint"; + } + } + + public sealed class GSCommentMessage : GSDomain + { + public const int MIN_LEN = 1; + public const int MAX_LEN = 1024; + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "varchar({0})".Args(MAX_LEN); + } + } + + public sealed class GSLike : GSDomain + { + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "int"; + } + } + + public sealed class GSPublicationState : GSEnum + { + public const int MAX_LEN = 1; + + public const string PRIVATE = "R"; + public const string PUBLIC = "P"; + public const string FRIEND = "F"; + public const string DELETED = "D"; + + public const string VALUE_LIST = "R: Private, P: Public, F:Friend, D:Deleted"; + + public static string ToDomainString(PublicationState status) + { + switch (status) + { + case PublicationState.Private: return GSPublicationState.PRIVATE; + case PublicationState.Public: return GSPublicationState.PUBLIC; + case PublicationState.Friend: return GSPublicationState.FRIEND; + case PublicationState.Deleted: return GSPublicationState.DELETED; + default: return GSPublicationState.PUBLIC; + } + } + + public static PublicationState ToPublicationState(string status) + { + if (status.EqualsOrdIgnoreCase(GSPublicationState.PRIVATE))return PublicationState.Private; + if (status.EqualsOrdIgnoreCase(GSPublicationState.PUBLIC)) return PublicationState.Public; + if (status.EqualsOrdIgnoreCase(GSPublicationState.FRIEND)) return PublicationState.Friend; + if (status.EqualsOrdIgnoreCase(GSPublicationState.DELETED)) return PublicationState.Deleted; + + return PublicationState.Public; + } + + public GSPublicationState() : base(DBCharType.Char, "C|P") { } + } + + public sealed class GSDimension : GSDomain + { + public const int MIN_LEN = 1; + public const int MAX_LEN = 8; // So we can pack in ulong + + public override string GetTypeName(RDBMSCompiler compiler) + { + return "char({0})".Args(MAX_LEN); + } + } +} diff --git a/src/Agni.Social/Graph/Server/Data/Schema/Graph.Comment.tables.MySQL.sql b/src/Agni.Social/Graph/Server/Data/Schema/Graph.Comment.tables.MySQL.sql new file mode 100644 index 0000000..38f49d4 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Schema/Graph.Comment.tables.MySQL.sql @@ -0,0 +1,43 @@ +delimiter ;. +-- Table tbl_comment +create table `tbl_comment` +( + `GDID` BINARY(12) not null, + `G_VOL` BINARY(12) not null comment 'Briefcase rating message', + `G_ATR` BINARY(12) not null comment 'Author', + `G_TRG` BINARY(12) not null comment 'What rated', + `DIM` char(8) not null comment 'Dimension', + `ROOT` CHAR(1) not null comment 'Is root', + `G_PAR` VARBINARY(12) comment 'This is a response to parent', + `MSG` varchar(1024) comment 'Comment message', + `DAT` VARBINARY(256) comment 'Comment message data. BSON', + `CDT` datetime(0) not null comment 'The UTC date of volume creation', + `LKE` int not null comment 'Liked', + `DIS` int not null comment 'Disliked', + `CMP` int not null comment 'Count of complaints', + `PST` CHAR(1) not null comment 'Publication state', + `RTG` tinyint not null comment 'Rating 1-5', + `RCNT` BIGINT(8) UNSIGNED not null comment 'Response count', + `IN_USE` CHAR(1) not null comment 'Logical Deletion flag', + constraint `pk_tbl_comment_primary` primary key (`GDID`) +) + comment = 'Hold comments and rating' +;. + +delimiter ;. +-- Table tbl_complaint +create table `tbl_complaint` +( + `GDID` BINARY(12) not null, + `G_CMT` BINARY(12) not null comment 'Reference to comment that complaint is for', + `G_ATH` BINARY(12) not null comment 'Author graph node', + `KND` char(8) not null comment 'Kind of complaint', + `MSG` varchar(1024) comment 'Complaint message', + `CDT` datetime(0) not null comment 'The UTC date of complaint creation', + `IN_USE` CHAR(1) not null comment 'Logical Deletion flag', + constraint `pk_tbl_complaint_primary` primary key (`GDID`) +) +;. + +delimiter ;. + create index `idx_tbl_complaint_cmt` on `tbl_complaint`(`G_CMT`);. diff --git a/src/Agni.Social/Graph/Server/Data/Schema/Graph.Node.tables.MySQL.sql b/src/Agni.Social/Graph/Server/Data/Schema/Graph.Node.tables.MySQL.sql new file mode 100644 index 0000000..7a0df80 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Schema/Graph.Node.tables.MySQL.sql @@ -0,0 +1,119 @@ +delimiter ;. +-- Table tbl_node +create table `tbl_node` +( + `GDID` BINARY(12) not null, + `TYP` char(8) not null comment 'Type of node such as: User, Forum, Club etc.', + `G_OSH` BINARY(12) not null comment 'Origin sharding GDID', + `G_ORI` BINARY(12) not null comment 'Origin GDID', + `ONM` varchar(48) not null comment 'Origin string name', + `ODT` VARBINARY(256) comment 'Origin data. BSON', + `CDT` datetime(0) not null comment 'The UTC date of node creation', + `FVI` CHAR(1) not null comment 'Default friend visibility', + `IN_USE` CHAR(1) not null comment 'Logical Deletion flag', + constraint `pk_tbl_node_primary` primary key (`GDID`) +) + comment = 'Holds Data about Graph Node which represent various socially-addressable entities in the host system, e.g. (PROD, 123, 45, \'SONY PLAYER\');(USR, 89, 23, \'Oleg Popov\')' +;. + +delimiter ;. + create index `idx_tbl_node_ori` on `tbl_node`(`G_ORI`) comment 'Used to locate records by origin, the type and origin shard are purposely omitted from this index';. +delimiter ;. +-- Table tbl_friendlist +create table `tbl_friendlist` +( + `GDID` BINARY(12) not null, + `G_OWN` BINARY(12) not null comment 'Graph node that has named list', + `LID` varchar(16) not null comment 'Friend list ID, such as \'Work\', \'Family\'', + `LDR` varchar(64) comment 'List description', + `CDT` datetime(0) not null comment 'When was created', + constraint `pk_tbl_friendlist_primary` primary key (`GDID`) +) + comment = 'Friend lists per node - a list is a named set of graph node connections, such as \'Family\', \'Coworkers\' etc.' +;. + +delimiter ;. + create index `idx_tbl_friendlist_own` on `tbl_friendlist`(`G_OWN`);. +delimiter ;. +-- Table tbl_subscribervol +create table `tbl_subscribervol` +( + `G_OWN` BINARY(12) not null comment 'Owner/emitter, briefcase key', + `G_VOL` BINARY(12) not null comment 'Briefcase for subscribers; generated from NODE id', + `CNT` BIGINT(8) UNSIGNED not null comment 'Approximate count of subscribers in briefcase', + `CDT` datetime(0) not null comment 'The UTC date of volume creation', + constraint `pk_tbl_subscribervol_primary` primary key (`G_OWN`, `G_VOL`) +) + comment = 'Subscription volume - splits large number of subscribers into a tree of volumes each sharded separately' +;. + +delimiter ;. +-- Table tbl_subscriber +create table `tbl_subscriber` +( + `G_VOL` BINARY(12) not null comment 'Who emits/to whom subscribed - briefcase key', + `G_SUB` BINARY(12) not null comment 'Who subscribes', + `STP` char(8) not null comment 'Type of node such as: User, Forum, Club etc.; denormalized from G_Subscriber for filtering', + `CDT` datetime(0) not null comment 'The UTC date of node subscription creation', + `PAR` VARBINARY(256) comment 'Subscription parameters - such as level of detail', + constraint `pk_tbl_subscriber_primary` primary key (`G_VOL`, `G_SUB`) +) + comment = 'Holds node subscribers, sharded on G_VOL' +;. + +delimiter ;. +-- Table tbl_commentvol +create table `tbl_commentvol` +( + `G_OWN` BINARY(12) not null comment 'Owner/target of comment, such as: product, service', + `G_VOL` BINARY(12) not null comment 'Briefcase of comment area', + `DIM` char(8) not null comment 'Dimension - such as \'review\', \'qna\'; a volume BELONGS to the particular dimension', + `CNT` BIGINT(8) UNSIGNED not null comment 'Approximate count of messages in briefcase', + `CDT` datetime(0) not null comment 'The UTC date of volume creation', + constraint `pk_tbl_commentvol_primary` primary key (`G_OWN`, `G_VOL`) +) + comment = 'Comment Volume - splits large number of comments into a tree of volumes each sharded in graph node area, kept separately in Comment Area' +;. + +delimiter ;. +-- Table tbl_friend +create table `tbl_friend` +( + `GDID` BINARY(12) not null, + `G_OWN` BINARY(12) not null comment 'A friend of WHO', + `G_FND` BINARY(12) not null comment 'A friend', + `RDT` datetime(0) not null comment 'The UTC date friend request', + `SDT` datetime(0) not null comment 'The UTC date of status', + `STS` CHAR(1) not null comment '[P]ending|[A]pproved|[D]enied|[B]anned', + `DIR` CHAR(1) not null comment '[I]am|[F]riend', + `VIS` CHAR(1) not null comment '[A]nyone|[P]ublic|[F]riend|[T]Private', + `LST` varchar(204) comment 'Friend lists comma-separated', + constraint `pk_tbl_friend_primary` primary key (`GDID`), + constraint `fk_tbl_friend_own` foreign key (`G_OWN`) references `tbl_node`(`GDID`) +) + comment = 'Holds node\'s friends. The list is capped by the system at 9999 including pending request and approved friends. 16000 including banned friends' +;. + +delimiter ;. + create unique index `idx_tbl_friend_uk` on `tbl_friend`(`G_OWN`, `G_FND`);. +delimiter ;. + create index `idx_tbl_friend_friend` on `tbl_friend`(`G_FND`, `G_OWN`);. +delimiter ;. +-- Table tbl_noderating +create table `tbl_noderating` +( + `G_NOD` BINARY(12) not null comment 'A Node', + `DIM` char(8) not null comment 'Dimension', + `CNT` BIGINT(8) UNSIGNED not null default '0' comment 'Count of comments (even with rating 0)', + `RTG1` BIGINT(8) UNSIGNED not null default '0' comment 'Count rating for value 1', + `RTG2` BIGINT(8) UNSIGNED not null default '0' comment 'Count rating for value 2', + `RTG3` BIGINT(8) UNSIGNED not null default '0' comment 'Count rating for value 3', + `RTG4` BIGINT(8) UNSIGNED not null default '0' comment 'Count rating for value 4', + `RTG5` BIGINT(8) UNSIGNED not null default '0' comment 'Count rating for value 5', + `CDT` datetime(0) not null comment 'The UTC date of node rating creation', + `LCD` datetime(0) not null comment 'The UTC date of change rating', + constraint `pk_tbl_noderating_primary` primary key (`G_NOD`, `DIM`) +) + comment = 'Rating node' +;. + diff --git a/src/Agni.Social/Graph/Server/Data/Schema/graph-comment.rschema b/src/Agni.Social/Graph/Server/Data/Schema/graph-comment.rschema new file mode 100644 index 0000000..0641b6b --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Schema/graph-comment.rschema @@ -0,0 +1,43 @@ +schema +{ + include="graph-common.rschema"{} + + table=Comment + { + comment="Hold comments and rating" + + _call=/scripts/gdid{} + + column=VOL { type=$(/$TRequiredGDIDRef) comment="Briefcase rating message" } + column=ATR { type=$(/$TRequiredGDIDRef) comment="Author" } + column=TRG { type=$(/$TRequiredGDIDRef) comment="What rated" } + + column=DIM { type=$(/$TDimension) required=true comment="Dimension"} + column=ROOT { type=$(/$TBool) required=true comment="Is root"} + column=PAR { type=$(/$TNullableGDIDRef) comment="This is a response to parent" } + column=MSG { type=$(/$TCommentMessage) required=false comment="Comment message"} + column=DAT { type=$(/$TBSONData) required=false comment="Comment message data. BSON"} + column=CDT { type=$(/$TTimestamp) required=true comment="The UTC date of volume creation"} + column=LKE { type=$(/$TLike) required=true comment="Liked"} + column=DIS { type=$(/$TLike) required=true comment="Disliked"} + column=CMP { type=$(/$TLike) required=true comment="Count of complaints"} + column=PST { type=$(/$TPublicationState) required=true comment="Publication state"} + column=RTG { type=$(/$TRating) required=true comment="Rating 1-5"} + column=RCNT { type=$(/$TCounter) required=true comment="Response count"} + + _call=/scripts/in-use{} + } + + table=complaint + { + _call=/scripts/gdid {} + column=CMT { type=$(/$TRequiredGDIDRef) comment="Reference to comment that complaint is for" } + column=ATH { type=$(/$TRequiredGDIDRef) comment="Author graph node" } + column=KND { type=$(/$TMessageType) required=true comment="Kind of complaint" } + column=MSG { type=$(/$TCommentMessage) required=false comment="Complaint message" } + column=CDT { type=$(/$TTimestamp) required=true comment="The UTC date of complaint creation" } + _call=/scripts/in-use{} + + index=cmt {column=CMT{}} + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Schema/graph-common.rschema b/src/Agni.Social/Graph/Server/Data/Schema/graph-common.rschema new file mode 100644 index 0000000..8abe027 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Schema/graph-common.rschema @@ -0,0 +1,49 @@ +schema +{ + PK_COLUMN = "GDID" + + TRUE='T' + FALSE='F' + + //Domain types used by Graph System + TRequiredGDID = "GSRequiredGDID" + TRequiredGDIDRef = "GSRequiredGDIDRef" + TNullableGDID = "GSNullableGDID" + TNullableGDIDRef = "GSNullableGDIDRef" + + TBool = "GSBool" + TCounter = "GSCounter" + + TRUE = 'T' + FALSE = 'F' + + TTimestamp = "GSTimestamp" + TDescription = "GSDescription" + TBSONData = "GSBSONData" + TParameters = "GSParameters" + + TNodeType = "GSNodeType" + TNodeName = "GSNodeName" + + TFriendListID = "GSFriendListID" + TFriendListIDs = "GSFriendListIDs" + TFriendStatus = "GSFriendStatus" + TFriendshipReqDirection = "GSFriendshipRequestDirection" + TFriendVisibility = "GSFriendVisibility" + + TMessageType = "GSMessageType" + + TRating = "GSRating" + TCommentMessage = "GSCommentMessage" + TLike = "GSLike" + + TPublicationState = "GSPublicationState" + TDimension = "GSDimension" + + scripts + { + script-only=true + gdid { column=$(/$PK_COLUMN) { type=$(/$TRequiredGDID)} primary-key{ column=$(/$PK_COLUMN){} } } + in-use { column=In_Use { type=$(/$TBool) required=true comment="Logical Deletion flag"} } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Schema/graph-node.rschema b/src/Agni.Social/Graph/Server/Data/Schema/graph-node.rschema new file mode 100644 index 0000000..c27edcb --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Schema/graph-node.rschema @@ -0,0 +1,140 @@ +schema +{ + include="graph-common.rschema"{} + + /* + NOTE + COLUMN NAMES are very short - this is done ON PURPOSE because + various databse protocol implementations may transmit column names in statements/fetches + while the Graph System is designed to handle hundredes of millions of rows per every server + with very many fetches/statements per second - the network traffic has to be minimized + */ + + table=Node + { + comment="Holds Data about Graph Node which represent various socially-addressable entities in the host system, e.g. (PROD, 123, 45, 'SONY PLAYER');(USR, 89, 23, 'Oleg Popov')" + + _call=/scripts/gdid{} + + column=TYP { type=$(/$TNodeType) required=true comment="Type of node such as: User, Forum, Club etc."} + column=OSH { type=$(/$TRequiredGDIDRef) comment="Origin sharding GDID"} + column=ORI { type=$(/$TRequiredGDIDRef) comment="Origin GDID"} + column=ONM { type=$(/$TNodeName) required=true comment="Origin string name"} + column=ODT { type=$(/$TBSONData) required=false comment="Origin data. BSON"} + column=CDT { type=$(/$TTimestamp) required=true comment="The UTC date of node creation"} + column=FVI { type=$(/$TFriendVisibility) required=true comment="Default friend visibility"} + + _call=/scripts/in-use{} + + index=ori + { + comment="Used to locate records by origin, the type and origin shard are purposely omitted from this index" + column=ORI{} + } + } + + + table=FriendList + { + comment="Friend lists per node - a list is a named set of graph node connections, such as 'Family', 'Coworkers' etc." + + _call=/scripts/gdid{} + + column=OWN { type=$(/$TRequiredGDIDRef) comment="Graph node that has named list" } + column=LID { type=$(/$TFriendListID) required=true comment="Friend list ID, such as 'Work', 'Family'"} + column=LDR { type=$(/$TDescription) required=false comment="List description"} + column=CDT { type=$(/$TTimestamp) required=true comment="When was created"} + + index=own + { + column=OWN{} + } + } + + + table=SubscriberVol + { + comment="Subscription volume - splits large number of subscribers into a tree of volumes each sharded separately" + + column=OWN { type=$(/$TRequiredGDIDRef) comment="Owner/emitter, briefcase key" } + column=VOL { type=$(/$TRequiredGDIDRef) comment="Briefcase for subscribers; generated from NODE id" } + column=CNT { type=$(/$TCounter) required=true comment="Approximate count of subscribers in briefcase"} + column=CDT { type=$(/$TTimestamp) required=true comment="The UTC date of volume creation"} + + primary-key {column=OWN{} column=VOL{}} + } + + table=Subscriber + { + comment="Holds node subscribers, sharded on G_VOL" + + column=VOL { type=$(/$TRequiredGDIDRef) comment="Who emits/to whom subscribed - briefcase key" } + column=SUB { type=$(/$TRequiredGDIDRef) comment="Who subscribes"} + column=STP { type=$(/$TNodeType) required=true comment="Type of node such as: User, Forum, Club etc.; denormalized from G_Subscriber for filtering"} + column=CDT { type=$(/$TTimestamp) required=true comment="The UTC date of node subscription creation"} + column=PAR { type=$(/$TBSONData) comment="Subscription parameters - such as level of detail"} + + primary-key {column=VOL{} column=SUB{}} + } + + table=CommentVol + { + comment="Comment Volume - splits large number of comments into a tree of volumes each sharded in graph node area, kept separately in Comment Area" + + column=OWN { type=$(/$TRequiredGDIDRef) comment="Owner/target of comment, such as: product, service" } + column=VOL { type=$(/$TRequiredGDIDRef) comment="Briefcase of comment area" } + column=DIM { type=$(/$TDimension) required=true comment="Dimension - such as 'review', 'qna'; a volume BELONGS to the particular dimension"} + column=CNT { type=$(/$TCounter) required=true comment="Approximate count of messages in briefcase"} + column=CDT { type=$(/$TTimestamp) required=true comment="The UTC date of volume creation"} + + primary-key { column=OWN{} column=VOL{} } + } + + table=Friend + { + comment= "Holds node's friends. The list is capped by the system at 9999 including pending request and approved friends. 16000 including banned friends" + + _call=/scripts/gdid{} + + column=OWN { type=$(/$TRequiredGDIDRef) comment="A friend of WHO" reference{ table="node" column=$(/$PK_COLUMN) }}//this can be referenced because friends are always briefcased in the same shard + column=FND { type=$(/$TRequiredGDIDRef) comment="A friend"} + column=RDT { type=$(/$TTimestamp) required=true comment="The UTC date friend request"} + column=SDT { type=$(/$TTimestamp) required=true comment="The UTC date of status"} + column=STS { type=$(/$TFriendStatus) required=true comment="[P]ending|[A]pproved|[D]enied|[B]anned"} + column=DIR { type=$(/$TFriendshipReqDirection) required=true comment="[I]am|[F]riend"} + column=VIS { type=$(/$TFriendVisibility) required=true comment="[A]nyone|[P]ublic|[F]riend|[T]Private"} + column=LST { type=$(/$TFriendListIDs) required=false comment="Friend lists comma-separated"} + + index=uk + { + unique=true + column=OWN{} + column=FND{} + } + + index=friend + { + column=FND{} + column=OWN{} + } + } + + table=NodeRating + { + comment="Rating node" + + column=NOD { type=$(/$TRequiredGDIDRef) comment="A Node" } + column=DIM { type=$(/$TDimension) required=true comment="Dimension" } + column=CNT { type=$(/$TCounter) required=true default=0 comment="Count of comments (even with rating 0)" } + column=RTG1 { type=$(/$TCounter) required=true default=0 comment="Count rating for value 1" } + column=RTG2 { type=$(/$TCounter) required=true default=0 comment="Count rating for value 2" } + column=RTG3 { type=$(/$TCounter) required=true default=0 comment="Count rating for value 3" } + column=RTG4 { type=$(/$TCounter) required=true default=0 comment="Count rating for value 4" } + column=RTG5 { type=$(/$TCounter) required=true default=0 comment="Count rating for value 5" } + column=CDT { type=$(/$TTimestamp) required=true comment="The UTC date of node rating creation" } + column=LCD { type=$(/$TTimestamp) required=true comment="The UTC date of change rating" } + + primary-key {column=NOD{} column=DIM{}} + } + +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/CountFriends.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/CountFriends.mys.sql new file mode 100644 index 0000000..78403b4 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/CountFriends.mys.sql @@ -0,0 +1 @@ +SELECT count(*) FROM tbl_friends WHERE G_OWN = ?pG_Node \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendByNode.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendByNode.mys.sql new file mode 100644 index 0000000..c25c46d --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendByNode.mys.sql @@ -0,0 +1 @@ +DELETE tbl_friend WHERE G_FND = ?pG_Node \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendByNodeAndFriend.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendByNodeAndFriend.mys.sql new file mode 100644 index 0000000..951e427 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendByNodeAndFriend.mys.sql @@ -0,0 +1 @@ +UPDATE tbl_friend SET STS = 'D', SDT = ?pdt WHERE G_OWN = ?pwon AND G_FND = ?pfnd \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendListByListId.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendListByListId.mys.sql new file mode 100644 index 0000000..f74e111 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/DeleteFriendListByListId.mys.sql @@ -0,0 +1,3 @@ +DELETE +FROM tbl_friendlist +WHERE GDID = ?pgnode AND LID = ?plistid; diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindAllFriends.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindAllFriends.mys.sql new file mode 100644 index 0000000..160d114 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindAllFriends.mys.sql @@ -0,0 +1 @@ +SELECT * FROM tbl_friends WHERE G_OWN = ?pNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindFriendListByNode.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindFriendListByNode.mys.sql new file mode 100644 index 0000000..d154fbc --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindFriendListByNode.mys.sql @@ -0,0 +1 @@ +SELECT * FROM tbl_friendlist WHERE G_OWN = ?pgnode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindFriends.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindFriends.mys.sql new file mode 100644 index 0000000..6dd78ac --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindFriends.mys.sql @@ -0,0 +1,6 @@ +SELECT * +FROM tbl_friends +WHERE G_OWN = ?pNode +AND LST LIKE ?pList +AND STS LIKE ?pStatus +LIMIT ?pFetchStart, ?pFetchCount diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindOneFriendByNodeAndFriend.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindOneFriendByNodeAndFriend.mys.sql new file mode 100644 index 0000000..79290cf --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/FindOneFriendByNodeAndFriend.mys.sql @@ -0,0 +1,3 @@ +SELECT * +FROM tbl_friend +WHERE G_OWN = ?pown AND G_FND = ?pfnd; diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/GetNextFriend.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/GetNextFriend.mys.sql new file mode 100644 index 0000000..7c296ec --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/GetNextFriend.mys.sql @@ -0,0 +1 @@ +SELECT * FROM tbl_friend WHERE G_OWN = ?pNode ORDER BY CDT LIMIT ?pStart, 1 \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/RemoveFriendByNode.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/RemoveFriendByNode.mys.sql new file mode 100644 index 0000000..aae07df --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/RemoveFriendByNode.mys.sql @@ -0,0 +1 @@ +DELETE tbl_friend WHERE G_OWN = ?gNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Friend/RemoveFriendListByNode.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/RemoveFriendListByNode.mys.sql new file mode 100644 index 0000000..d07ed10 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Friend/RemoveFriendListByNode.mys.sql @@ -0,0 +1 @@ +DELETE tbl_friendlist WHERE G_OWN = ?gNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/CountSubscriberVolumes.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/CountSubscriberVolumes.mys.sql new file mode 100644 index 0000000..447935a --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/CountSubscriberVolumes.mys.sql @@ -0,0 +1 @@ +SELECT count(*) AS CNT FROM tbl_subvol WHERE G_OWN = ?pNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/CountSubscribers.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/CountSubscribers.mys.sql new file mode 100644 index 0000000..543fde4 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/CountSubscribers.mys.sql @@ -0,0 +1 @@ +SELECT sum(cnt) AS CNT FROM tbl_subvol WHERE G_OWN = ?pNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/DeleteOneNodeByGDID.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/DeleteOneNodeByGDID.mys.sql new file mode 100644 index 0000000..36384cc --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/DeleteOneNodeByGDID.mys.sql @@ -0,0 +1 @@ +UPDATE tbl_node SET IN_USE = ?pInUse WHERE GDID = ?pgnode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindOneNodeByGDID.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindOneNodeByGDID.mys.sql new file mode 100644 index 0000000..54bff91 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindOneNodeByGDID.mys.sql @@ -0,0 +1,11 @@ +SELECT GDID AS GDID +, TYP AS Node_Type +, G_OSH AS G_OriginShard +, G_ORI AS G_Origin +, ONM AS Origin_Name +, ODT AS Origin_Data +, CDT AS Create_Date +, FVI AS Friend_Visibility +, IN_USE AS In_Use +FROM tbl_node +WHERE GDID = ?pgnode AND IN_USE = 'T' \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscriber.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscriber.mys.sql new file mode 100644 index 0000000..d8c0907 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscriber.mys.sql @@ -0,0 +1,8 @@ +SELECT G_VOL AS G_SubscriberVolume +, G_SUB AS G_Subscriber +, STP AS Subs_Type +, CDT AS Create_Date +, PAR AS Parameters +FROM tbl_subscriber +WHERE G_VOL = ?pVol +AND G_SUB = ?pSub \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscriberVolumes.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscriberVolumes.mys.sql new file mode 100644 index 0000000..42eb3ae --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscriberVolumes.mys.sql @@ -0,0 +1,7 @@ +SELECT G_OWN AS G_Owner +, G_VOL AS G_SubscriberVolume +, CNT AS Count +, CDT AS Create_Date +FROM tbl_subvol +WHERE G_OWN = ?pNode +ORDER BY CDT \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscribers.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscribers.mys.sql new file mode 100644 index 0000000..9694d7c --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/FindSubscribers.mys.sql @@ -0,0 +1,9 @@ +SELECT G_VOL AS G_SubscriberVolume +, G_SUB AS G_Subscriber +, STP AS Subs_Type +, CDT AS Create_Date +, PAR AS Parameters +FROM tbl_subscriber +WHERE G_VOL = ?pVol +ORDER BY G_VOL, G_SUB +LIMIT ?pStart, ?pCount diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/GetNextVolume.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/GetNextVolume.mys.sql new file mode 100644 index 0000000..fd3fd92 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/GetNextVolume.mys.sql @@ -0,0 +1,8 @@ +SELECT G_OWN AS G_Owner +, G_VOL AS G_SubscriberVolume +, CNT AS Count +, CDTCreate_Date +FROM tbl_subvol +WHERE G_OWN = ?pNode +ORDER BY CDT +LIMIT ?pStart, 1 \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveNode.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveNode.mys.sql new file mode 100644 index 0000000..7cebe3a --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveNode.mys.sql @@ -0,0 +1 @@ +DELETE tbl_node WHERE GDID = ?gNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveSubVol.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveSubVol.mys.sql new file mode 100644 index 0000000..3123d31 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveSubVol.mys.sql @@ -0,0 +1 @@ +DELETE tbl_subscriber WHERE G_OWN = ?gNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveSubscribers.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveSubscribers.mys.sql new file mode 100644 index 0000000..d5e5a87 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Node/RemoveSubscribers.mys.sql @@ -0,0 +1 @@ +DELETE tbl_node WHERE G_VOL = ?gVol \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/CancelComplaintsByComment.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/CancelComplaintsByComment.mys.sql new file mode 100644 index 0000000..83a7252 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/CancelComplaintsByComment.mys.sql @@ -0,0 +1 @@ +UPDATE tbl_complaint SET IN_USE = 'F' WHERE G_CMT = ?pG_Comment AND IN_USE = 'T' \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/ClearNodeRating.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/ClearNodeRating.mys.sql new file mode 100644 index 0000000..4e325dd --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/ClearNodeRating.mys.sql @@ -0,0 +1 @@ +UPDATE tbl_noderating SET RTG1=0, RTG2=0, RTG3=0, RTG4=0, RTG5=0, CTD=?pCDT, WHERE G_NOD = ?pNode AND DIM = ?pDIM \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/ClearNodeRatings.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/ClearNodeRatings.mys.sql new file mode 100644 index 0000000..3c82ecb --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/ClearNodeRatings.mys.sql @@ -0,0 +1 @@ +UPDATE tbl_noderating SET RTG1=0, RTG2=0, RTG3=0, RTG4=0, RTG5=0, CTD=?pCDT, WHERE G_NOD = ?pNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/CountResponses.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/CountResponses.mys.sql new file mode 100644 index 0000000..d2c9a59 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/CountResponses.mys.sql @@ -0,0 +1 @@ +SELECT count(*) AS CNT FROM tbl_comment WHERE G_PAR = ?pComment AND G_VOL = ?pVolume \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/DeleteResponses.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/DeleteResponses.mys.sql new file mode 100644 index 0000000..d5dc7d9 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/DeleteResponses.mys.sql @@ -0,0 +1 @@ +DELETE FROM tbl_comment WHERE G_PAR = ?pParent AND G_VOL = ?gVolume \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentByGDID.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentByGDID.mys.sql new file mode 100644 index 0000000..c637852 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentByGDID.mys.sql @@ -0,0 +1,18 @@ +SELECT GDID +, G_VOL AS G_CommentVolume +, G_ATR AS G_AuthorNode +, G_TRG AS G_TargetNode +, DIM AS Dimension +, ROOT AS IsRoot +, G_PAR AS G_Parent +, MSG AS Message +, DAT AS Data +, CDT AS Create_Date +, LKE AS "Like" +, DIS AS Dislike +, CMP AS ComplaintCount +, PST AS PublicationState +, RTG AS Rating +, In_Use +, RCNT as ResponseCount +FROM tbl_comment WHERE GDID = ?pGDID \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentVolume.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentVolume.mys.sql new file mode 100644 index 0000000..1be6544 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentVolume.mys.sql @@ -0,0 +1,7 @@ +SELECT G_OWN AS G_Owner +, G_VOL AS G_CommentVolume +, DIM AS Dimension +, CNT AS Count +, CDT AS Create_Date +FROM tbl_commentvol +WHERE G_OWN = ?pNode AND G_VOL = ?pVol \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentVolumes.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentVolumes.mys.sql new file mode 100644 index 0000000..a6d235a --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindCommentVolumes.mys.sql @@ -0,0 +1,6 @@ +SELECT G_OWN AS G_Owner +, G_VOL AS G_CommentVolume +, DIM AS Dimension +, CNT AS Count +, CDT AS Create_Date +FROM tbl_commentvol WHERE G_OWN = ?pNode AND DIM = ?pDim AND CDT <= ?pCDT ORDER BY CDT DESC LIMIT 0,?cnt \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindComments.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindComments.mys.sql new file mode 100644 index 0000000..dd76a29 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindComments.mys.sql @@ -0,0 +1,19 @@ +SELECT GDID +, G_VOL AS G_CommentVolume +, G_ATR AS G_AuthorNode +, G_TRG AS G_TargetNode +, DIM AS Dimension +, ROOT AS IsRoot +, G_PAR AS G_Parent +, MSG AS Message +, DAT AS Data +, CDT AS Create_Date +, LKE AS "Like" +, DIS AS Dislike +, CMP AS ComplaintCount +, PST AS PublicationState +, RTG AS Rating +, In_Use +, RCNT as ResponseCount +FROM tbl_comment +WHERE (G_TRG = ?pNode) AND (DIM = ?pDim) AND (ROOT = ?pRoot) AND (In_Use = 'T' OR RCNT > 0) \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindComplaints.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindComplaints.mys.sql new file mode 100644 index 0000000..cd10c40 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindComplaints.mys.sql @@ -0,0 +1,10 @@ +SELECT + G_CMT AS G_Comment, + GDID, + G_ATH AS G_AuthorNode, + KND AS Kind, + MSG AS Message, + CDT AS Create_Date, + In_Use +FROM tbl_complaint +WHERE G_CMT = ?pComment \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindEmptyCommentVolume.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindEmptyCommentVolume.mys.sql new file mode 100644 index 0000000..735af61 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindEmptyCommentVolume.mys.sql @@ -0,0 +1,9 @@ +SELECT G_OWN AS G_Owner +, G_VOL AS G_CommentVolume +, DIM AS Dimension +, CNT AS Count +, CDT AS Create_Date +FROM tbl_commentvol +WHERE G_OWN = ?pNode AND DIM = ?pDim AND CNT < ?pMaxCount +ORDER BY CDT DESC +LIMIT 0,1 \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindNodeRating.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindNodeRating.mys.sql new file mode 100644 index 0000000..0e9a5ac --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindNodeRating.mys.sql @@ -0,0 +1,15 @@ +SELECT G_NOD AS G_Node +, CDT AS Create_Date +, DIM AS Dimension +, CNT AS Cnt +, LCD AS Last_Change_Date +, RTG1 AS Rating1 +, RTG2 AS Rating2 +, RTG3 AS Rating3 +, RTG4 AS Rating4 +, RTG5 AS Rating5 +FROM tbl_noderating +WHERE G_NOD = ?pNode + AND DIM = ?pDim +ORDER BY CDT DESC +LIMIT 1 \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindNodeRatings.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindNodeRatings.mys.sql new file mode 100644 index 0000000..38091db --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindNodeRatings.mys.sql @@ -0,0 +1,11 @@ +SELECT G_NOD AS G_Node +, CDT AS Create_Date +, DIM AS Dimension +, CNT AS Cnt +, LCD AS Last_Change_Date +, RTG1 AS Rating1 +, RTG2 AS Rating2 +, RTG3 AS Rating3 +, RTG4 AS Rating4 +, RTG5 AS Rating5 +FROM tbl_noderating WHERE CDT <= ?pDT AND G_NOD = ?pNode \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindResponses.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindResponses.mys.sql new file mode 100644 index 0000000..c9973b5 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindResponses.mys.sql @@ -0,0 +1,18 @@ +SELECT G_VOL AS G_CommentVolume +, GDID +, G_ATR AS G_AuthorNode +, G_TRG AS G_TargetNode +, DIM AS Dimension +, ROOT AS IsRoot +, G_PAR AS G_Parent +, MSG AS Message +, DAT AS Data +, CDT AS Create_Date +, LKE AS "Like" +, DIS AS Dislike +, CMP AS Complaint +, PST AS PublicationState +, RTG AS Rating +, In_Use +, RCNT as ResponseCount +FROM tbl_comment WHERE G_PAR = ?pComment AND G_VOL = ?pVolume AND (In_Use = 'T' OR (In_Use = 'F' AND RCNT > 0)) \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindVolumeComment.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindVolumeComment.mys.sql new file mode 100644 index 0000000..735af61 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/FindVolumeComment.mys.sql @@ -0,0 +1,9 @@ +SELECT G_OWN AS G_Owner +, G_VOL AS G_CommentVolume +, DIM AS Dimension +, CNT AS Count +, CDT AS Create_Date +FROM tbl_commentvol +WHERE G_OWN = ?pNode AND DIM = ?pDim AND CNT < ?pMaxCount +ORDER BY CDT DESC +LIMIT 0,1 \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/HasCommentsCreatedByAuthor.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/HasCommentsCreatedByAuthor.mys.sql new file mode 100644 index 0000000..c47453d --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/HasCommentsCreatedByAuthor.mys.sql @@ -0,0 +1,5 @@ +SELECT * + FROM tbl_comment +WHERE + DIM = ?pDim AND G_ATR = ?pAuthor AND G_TRG = ?pNode AND ROOT = 'T' AND In_Use = 'T' +LIMIT 1 diff --git a/src/Agni.Social/Graph/Server/Data/Scripts/Rating/UpdateLike.mys.sql b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/UpdateLike.mys.sql new file mode 100644 index 0000000..6bc7db2 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/Scripts/Rating/UpdateLike.mys.sql @@ -0,0 +1 @@ +UPDATE tbl_comment SET LKE=LKE+?pdtLike, DIS=DIS+?pdtDislike WHERE GDID = ?pComment AND G_VOL = ?pVolume \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Data/SubscriberRow.cs b/src/Agni.Social/Graph/Server/Data/SubscriberRow.cs new file mode 100644 index 0000000..6987520 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Data/SubscriberRow.cs @@ -0,0 +1,65 @@ +using System; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; + +using Agni.Social.Graph.Server.Data.Schema; +using NFX.Serialization.Arow; + +namespace Agni.Social.Graph.Server.Data +{ + + /// + /// Logical chunk of subscribers + /// + [Table(name: "tbl_subscribervol")] + public sealed class SubscriberVolumeRow : BaseRow + { + /// Owner GDID; Briefcase key + [Field(backendName: "G_OWN", required: true, key: true)] + public GDID G_Owner { get; set; } + + /// + /// Link for Subscriber Volume. + /// G_VOL is generated of NodeRow GDID sequence + /// + [Field(backendName: "G_VOL", required: true, key: true)] + public GDID G_SubscriberVolume { get; set; } + + /// Count subscriber in volume + [Field(backendName: "CNT", required: true)] + public int Count { get; set; } + + /// Create date volume + [Field(backendName: "CDT", required: true)] + public DateTime Create_Date { get; set; } + + } + + /// + /// Lists all subscribers per volume; sharded in the graph area + /// + [Table(name: "tbl_subscriber")] + public sealed class SubscriberRow : BaseRow + { + /// Emitter - briefcase key + [Field(backendName: "G_VOL", required: true, key: true)] + public GDID G_SubscriberVolume { get; set; } + + /// Subscriber + [Field(backendName: "G_SUB", required: true, key: true)] + public GDID G_Subscriber { get; set; } + + /// Denormalizes node type from G_Subscriber for faster search + [Field(backendName: "STP", required: true, minLength: GSNodeType.MIN_LEN, maxLength: GSNodeType.MAX_LEN)] + public string Subs_Type { get; set; } + + [Field(backendName: "CDT", required: true)] + public DateTime Create_Date { get; set; } + + /// Subscription details + [Field(backendName: "PAR", required: false)] + public byte[] Parameters { get; set; } + } +} diff --git a/src/Agni.Social/Graph/Server/GraphCommentFetchDefaultStrategy.cs b/src/Agni.Social/Graph/Server/GraphCommentFetchDefaultStrategy.cs new file mode 100644 index 0000000..0dcb71e --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphCommentFetchDefaultStrategy.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; + +using NFX; +using NFX.ApplicationModel.Pile; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Environment; + +using Agni.MDB; +using Agni.Social.Graph.Server.Data; +using Agni.Social.Graph.Server.Data.Schema; + +namespace Agni.Social.Graph.Server +{ + public class GraphCommentFetchDefaultStrategy : DisposableObject, IConfigurable + { + + public GraphCommentFetchDefaultStrategy(GraphSystemService graphSystemService, IConfigSectionNode config) + { + m_GraphSystemService = graphSystemService; + m_ConfigNode = config; + ConfigAttribute.Apply(this, config); + } + + protected GraphSystemService m_GraphSystemService; + protected IConfigSectionNode m_ConfigNode; + + protected GraphHost GraphHost { get { return GraphOperationContext.Instance.GraphHost; } } + protected IMDBDataStore DataStore { get { return m_GraphSystemService.DataStore; } } + protected ICache Cache { get { return m_GraphSystemService.Cache; } } + protected int MaxLastCommentVolumes { get { return GraphSystemService.MAX_SCAN_TAIL_COMMENT_VOLUMES; } } + + public void Configure(IConfigSectionNode node) + { + + } + + public virtual IEnumerable Fetch(CommentQuery query) + { + var start = query.BlockIndex * CommentQuery.COMMENT_BLOCK_SIZE; + var comments = Cache.FetchThrough>(query, + SocialConsts.GS_COMMENT_BLOCK_TBL, + CacheParams.ReadFreshWriteSec(0), // todo TO DISCUSS + commQry => + { + var rows = new List(); + var qryVolumes = Queries.FindCommentVolumes(query.G_TargetNode, query.Dimension, MaxLastCommentVolumes, query.AsOfDate); + var volumes = ForNode(query.G_TargetNode).LoadEnumerable(qryVolumes); + foreach (var volume in volumes) + { + var qryComments = Queries.FindComments(query.G_TargetNode, query.Dimension, true); + var loadRows = ForComment(volume.G_CommentVolume).LoadEnumerable(qryComments); + rows.AddRange(loadRows); + } + + IEnumerable orderedRows = null; + + switch (query.OrderType) + { + case CommentOrderType.ByDate: + if (query.Ascending) + orderedRows = rows.OrderBy(row => row.Create_Date); + else + orderedRows = rows.OrderByDescending(row => row.Create_Date); + break; + case CommentOrderType.ByPositive: + orderedRows = rows.OrderBy(row => row.Rating) + .ThenByDescending(row => row.Create_Date); + break; + case CommentOrderType.ByNegative: + orderedRows = rows.OrderByDescending(row => row.Rating) + .ThenByDescending(row => row.Create_Date); + break; + case CommentOrderType.ByPopular: + if (query.Ascending) + orderedRows = rows.OrderBy(row => row.Like + row.Dislike) + .ThenByDescending(row => row.Create_Date); + else + orderedRows = rows.OrderByDescending(row => row.Like + row.Dislike) + .ThenByDescending(row => row.Create_Date); + break; + case CommentOrderType.ByUsefull: + if (query.Ascending) + orderedRows = rows.OrderBy(row => row.Like - row.Dislike) + .ThenByDescending(row => row.Create_Date); + else + orderedRows = rows.OrderByDescending(row => row.Like - row.Dislike) + .ThenByDescending(row => row.Create_Date); + break; + default: + orderedRows = rows.OrderByDescending(row => row.Create_Date); + break; + } + return orderedRows.Skip(start) + .Take(CommentQuery.COMMENT_BLOCK_SIZE) + .Select(RowToComment).ToArray(); + }); + + return comments; + } + + public CRUDOperations ForNode(GDID gNode) + { + return DataStore.PartitionedOperationsFor(SocialConsts.MDB_AREA_NODE, gNode); + } + + protected CRUDOperations ForComment(GDID gVolume) + { + return DataStore.PartitionedOperationsFor(SocialConsts.MDB_AREA_COMMENT, gVolume); + } + + public Comment RowToComment(CommentRow row) + { + if (row == null) + return default(Comment); + var commentID = new CommentID(row.G_CommentVolume, row.GDID); + var parentID = row.G_Parent.HasValue ? new CommentID(row.G_CommentVolume, row.G_Parent.Value) : (CommentID?)null; + var authorNode = m_GraphSystemService.GetNode(row.G_AuthorNode); + var targetNode = m_GraphSystemService.GetNode(row.G_TargetNode); + var editableTimespan = GraphHost.EditCommentSpan(targetNode, row.Dimension); + var lifeTime = App.TimeSource.UTCNow - row.Create_Date; + + return new Comment(commentID, + parentID, + authorNode, + targetNode, + row.Create_Date, + row.Dimension, + GSPublicationState.ToPublicationState(row.PublicationState), + row.IsRoot ? (RatingValue)row.Rating : RatingValue.Undefined, + row.Message, + row.Data, + row.Like, + row.Dislike, + row.ComplaintCount, + row.ResponseCount, + row.In_Use, + editableTimespan > lifeTime); + } + } +} diff --git a/src/Agni.Social/Graph/Server/GraphHost.cs b/src/Agni.Social/Graph/Server/GraphHost.cs new file mode 100644 index 0000000..80eb607 --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphHost.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; + +using NFX; +using NFX.ApplicationModel; +using NFX.Environment; +using NFX.DataAccess.CRUD; + +using Agni.Social.Graph.Server.Data; + +namespace Agni.Social.Graph.Server +{ + /// + /// Provides hosting API for the graph system in the particular business system scope + /// + public abstract class GraphHost : ApplicationComponent, IConfigurable + { + + protected GraphHost(GraphOperationContext director, IConfigSectionNode config) : base(director) + { + ConfigAttribute.Apply(this, config); + } + + /// + /// Limits the number of responses to comments + /// + public abstract int MaxResponseForComment { get; } + + /// + /// Perform actual work of event delivery. + /// Returns true if event was (scheduled-to be) delivered + /// + public bool DeliverEvent(GraphNode nodeRecipient, GraphNode nodeSender, Event evt, string subscriptionParameters) + { + return DoDeliverEvent(nodeRecipient, nodeSender, evt, subscriptionParameters); + } + + //filter event batch start + //filter event recipient + //filter event batch end + + public void Configure(IConfigSectionNode node) + { + DoConfigure(node); + } + + + /// + /// Convert binary data to Row Node + /// + public abstract TypedRow NodeBinaryDataToObject(string nodeType, byte[] data); + + /// + /// Convert Row Node to binary data + /// + public abstract byte[] ObjectToNodeBinaryData(string nodeType, TypedRow data); + + /// + /// Filter by query + /// + public abstract IEnumerable FilterByOriginQuery(IEnumerable data, string orgQry); + + /// + /// + /// + public abstract IEnumerable FilterEventsChunk(IEnumerable subscribersChunk, Event evt, IConfigSectionNode cfg); + + /// + /// This function must not leak exceptions + /// Returns the subscriptions that could not be delivered now - the system will try to redeliver them later asynchronously + /// + public abstract IEnumerable DeliverEventsChunk(IEnumerable filtered, Event evt, IConfigSectionNode cfg); + + /// + /// Returns true if specified node types can be friends in the specified direction. + /// Keep in mind that friendship is bidirectional so this method only checks the initiating party + /// + public abstract bool CanBeFriends(string fromNodeType, string toNodeType); + + /// + /// Returns true if specified node types can be subscribed + /// + public abstract bool CanBeSubscribed(string subscriberNodeType, string emitterNodeType); + + #region Graph comment + + /// + /// Returns true if specified node type can rate other nodes + /// + public abstract bool CanBeRatingActor(string ratingNodeType, string dimension); + + /// + /// Returns true if the specified node type requires rating for the specified dimension + /// + public abstract bool CommentRatingRequired(string ratedNodeType, string dimension); + + /// + /// Returns a span of time within which a comment can be edited + /// + public abstract TimeSpan EditCommentSpan(GraphNode targetNode, string dimension); + + /// + /// Returns true if authorNode can create new comment of specified targetNode as of commentDate and + /// if rating value can be applied + /// + public virtual bool CanCreateComment(GraphNode authorNode, + GraphNode targetNode, + string dimension, + DateTime commentDate, + RatingValue rating) + { + var ratingRequired = CommentRatingRequired(targetNode.NodeType, dimension); + + if (ratingRequired && rating == RatingValue.Undefined) + return false; + + if (ratingRequired && !CanBeRatingActor(authorNode.NodeType, dimension)) + return false; + + return DoCanCreateComment(authorNode, targetNode, dimension, commentDate, rating); + } + + /// + /// Returns true if authorNode can create a response to the specified comment as of commentDate and + /// if rating value can be applied + /// + public virtual bool CanCreateCommentResponse(Comment parent, GraphNode authorNode, DateTime responseDate) + { + if (parent.ResponseCount > MaxResponseForComment) return false; + + return DoCanCreateCommentResponse(parent, authorNode, responseDate); + } + + /// + /// A hook invoked upon physical deletion of comment + /// + public abstract void DeleteComment(GraphNode authorNode, CommentID comment); + + #endregion + + protected abstract void DoConfigure(IConfigSectionNode node); + + protected abstract bool DoDeliverEvent(GraphNode nodeRecipient, GraphNode nodeSender, Event evt, string subscriptionParameters); + + protected abstract bool DoCanCreateComment(GraphNode authorNode, + GraphNode targetNode, + string dimension, + DateTime commentDate, + RatingValue rating); + + protected abstract bool DoCanCreateCommentResponse(Comment parent, GraphNode authorNode, DateTime responseDate); + } +} diff --git a/src/Agni.Social/Graph/Server/GraphOperationContext.cs b/src/Agni.Social/Graph/Server/GraphOperationContext.cs new file mode 100644 index 0000000..09848a8 --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphOperationContext.cs @@ -0,0 +1,92 @@ +using Agni.MDB; +using Agni.WebMessaging; +using NFX; +using NFX.ApplicationModel; +using NFX.Environment; + +namespace Agni.Social.Graph.Server +{ + /// + /// Provides context for graph operations: graph config, datastore, and host + /// + public sealed class GraphOperationContext : ApplicationComponent, IApplicationStarter, IApplicationFinishNotifiable + { + + private static object s_Lock = new object(); + private static volatile GraphOperationContext s_Instance; + + public static GraphOperationContext Instance + { + get + { + var instance = s_Instance; + if (instance == null) + throw new GraphException(StringConsts.GS_INSTANCE_DATA_LAYER_IS_NOT_ALLOCATED_ERROR.Args(typeof(GraphSystemService).Name)); + return instance; + } + } + + private GraphOperationContext() + { + lock (s_Lock) + { + if (s_Instance != null) + throw new WebMessagingException(StringConsts.GS_INSTANCE_ALREADY_ALLOCATED_ERROR.Args(GetType().Name)); + s_Instance = this; + } + } + + protected override void Destructor() + { + lock (s_Lock) + { + base.Destructor(); + + DisposableObject.DisposeAndNull(ref m_GraphHost); + DisposableObject.DisposeAndNull(ref m_DataStore); + + s_Instance = null; + } + } + + private IConfigSectionNode m_Config; + private MDBDataStore m_DataStore; + private GraphHost m_GraphHost; + + public string Name { get { return GetType().Name; } } + public IMDBDataStore DataStore {get { return m_DataStore; }} + public GraphHost GraphHost { get { return m_GraphHost; }} + + bool IApplicationStarter.ApplicationStartBreakOnException { get { return true; } } + + void IApplicationStarter.ApplicationStartBeforeInit(IApplication application) {} + + void IApplicationStarter.ApplicationStartAfterInit(IApplication application) + { + application.RegisterAppFinishNotifiable(this); + + var nDataSore = m_Config[SocialConsts.CONFIG_DATA_STORE_SECTION]; + if(!nDataSore.Exists) throw new SocialException(StringConsts.GS_INIT_NOT_CONF_ERRROR.Args(this.GetType().Name, SocialConsts.CONFIG_DATA_STORE_SECTION)); + m_DataStore = FactoryUtils.MakeAndConfigure(nDataSore, args: new object[] { "GraphSystem", this }); + m_DataStore.Start(); + + var nHost = m_Config[SocialConsts.CONFIG_GRAPH_HOST_SECTION]; + if (!nHost.Exists) throw new SocialException(StringConsts.GS_INIT_NOT_CONF_ERRROR.Args(this.GetType().Name, SocialConsts.CONFIG_GRAPH_HOST_SECTION)); + m_GraphHost = FactoryUtils.MakeAndConfigure(nHost, args: new object[]{ this, nHost }); + } + + void IApplicationFinishNotifiable.ApplicationFinishBeforeCleanup(IApplication application) + { + Dispose(); + } + + void IApplicationFinishNotifiable.ApplicationFinishAfterCleanup(IApplication application) { } + + void IConfigurable.Configure(IConfigSectionNode node) + { + m_Config = node; + + ConfigAttribute.Apply(this, node); + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/GraphServers.cs b/src/Agni.Social/Graph/Server/GraphServers.cs new file mode 100644 index 0000000..05ce887 --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphServers.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph.Server +{ + public sealed class GraphNodeSystemServer : IGraphNodeSystem + { + public IGraphNodeSystem Nodes { get { return GraphSystemService.Instance; } } + + public GraphChangeStatus SaveNode(GraphNode node) + { + return Nodes.SaveNode(node); + } + + public GraphNode GetNode(GDID gNode) + { + return Nodes.GetNode(gNode); + } + + public GraphChangeStatus DeleteNode(GDID gNode) + { + return Nodes.DeleteNode(gNode); + } + + public GraphChangeStatus UndeleteNode(GDID gNode) + { + return Nodes.UndeleteNode(gNode); + } + + public GraphChangeStatus RemoveNode(GDID gNode) + { + return Nodes.RemoveNode(gNode); + } + } + + public sealed class GraphCommentSystemServer : IGraphCommentSystem + { + public IGraphCommentSystem Comments { get { return GraphSystemService.Instance; } } + + /// + /// Create new comment with rating + /// + /// GDID of autor node + /// GDID of target node + /// Scope for rating + /// Content + /// Byte array of data + /// State of publication + /// star 1-2-3-4-5 + /// Time of current action + /// Comment + public Comment Create(GDID gAuthorNode, + GDID gTargetNode, + string dimension, + string content, + byte[] data, + PublicationState publicationState, + RatingValue value = RatingValue.Undefined, + DateTime? timeStamp = null) + { + return Comments.Create(gAuthorNode, gTargetNode, dimension, content, data, publicationState, value, timeStamp); + } + + /// + /// Make response on target commentary + /// + /// Author + /// Parent commentary + /// Content of commentary + /// Byte Array + /// New Comment + public Comment Respond(GDID gAuthorNode, CommentID parent, string content, byte[] data) + { + return Comments.Respond(gAuthorNode, parent, content, data); + } + + public GraphChangeStatus Update(CommentID commentId, RatingValue value, string content, byte[] data) + { + return Comments.Update(commentId, value, content, data); + } + + /// + /// Delete comment + /// + /// Existing Comment ID + public GraphChangeStatus DeleteComment(CommentID commentId) + { + return Comments.DeleteComment(commentId); + } + + public GraphChangeStatus Like(CommentID commentId, int deltaLike, int deltaDislike) + { + return Comments.Like(commentId, deltaLike, deltaDislike); + } + + public bool IsCommentedByAuthor(GDID gNode, GDID gAuthor, string dimension) + { + return Comments.IsCommentedByAuthor(gNode, gAuthor, dimension); + } + + public IEnumerable GetNodeSummaries(GDID gNode) + { + return Comments.GetNodeSummaries(gNode); + } + + public IEnumerable Fetch(CommentQuery query) + { + return Comments.Fetch(query); + } + + public IEnumerable FetchResponses(CommentID commentId) + { + return Comments.FetchResponses(commentId); + } + + public IEnumerable FetchComplaints(CommentID commentID) + { + return Comments.FetchComplaints(commentID); + } + + public Comment GetComment(CommentID commentId) + { + return Comments.GetComment(commentId); + } + + public GraphChangeStatus Complain(CommentID commentId, GDID gAuthorNode, string kind, string message) + { + return Comments.Complain(commentId, gAuthorNode, kind, message); + } + + public GraphChangeStatus Justify(CommentID commentID) + { + return Comments.Justify(commentID); + } + } + + public sealed class GraphEventSystemServer : IGraphEventSystem + { + public IGraphEventSystem Events { get { return GraphSystemService.Instance; } } + + public void EmitEvent(Event evt) + { + Events.EmitEvent(evt); + } + + public void Subscribe(GDID gRecipientNode, GDID gEmitterNode, byte[] parameters) + { + Events.Subscribe(gRecipientNode, gEmitterNode, parameters); + } + + public void Unsubscribe(GDID gRecipientNode, GDID gEmitterNode) + { + Events.Unsubscribe(gRecipientNode, gEmitterNode); + } + + public long EstimateSubscriberCount(GDID gEmitterNode) + { + return Events.EstimateSubscriberCount(gEmitterNode); + } + + public IEnumerable GetSubscribers(GDID gEmitterNode, long start, int count) + { + return Events.GetSubscribers(gEmitterNode, start, count); + } + } + + public sealed class GraphFriendSystemServer : IGraphFriendSystem + { + public IGraphFriendSystem Friends { get { return GraphSystemService.Instance; } } + + public IEnumerable GetFriendLists(GDID gNode) + { + return Friends.GetFriendLists(gNode); + } + + public GraphChangeStatus AddFriendList(GDID gNode, string list, string description) + { + return Friends.AddFriendList(gNode, list, description); + } + + public GraphChangeStatus DeleteFriendList(GDID gNode, string list) + { + return Friends.DeleteFriendList(gNode, list); + } + + public IEnumerable GetFriendConnections(FriendQuery query) + { + return Friends.GetFriendConnections(query); + } + + public GraphChangeStatus AddFriend(GDID gNode, GDID gFriendNode, bool? approve) + { + return Friends.AddFriend(gNode, gFriendNode, approve); + } + + public GraphChangeStatus AssignFriendLists(GDID gNode, GDID gFriendNode, string lists) + { + return Friends.AssignFriendLists(gNode, gFriendNode, lists); + } + + public GraphChangeStatus DeleteFriend(GDID gNode, GDID gFriendNode) + { + return Friends.DeleteFriend(gNode, gFriendNode); + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/GraphSystemService.Comment.cs b/src/Agni.Social/Graph/Server/GraphSystemService.Comment.cs new file mode 100644 index 0000000..c70d6da --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphSystemService.Comment.cs @@ -0,0 +1,655 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using NFX; +using NFX.DataAccess.Distributed; +using NFX.Log; + +using Agni.MDB; +using Agni.Social.Graph.Server.Data; +using Agni.Social.Graph.Server.Data.Schema; + +namespace Agni.Social.Graph.Server +{ + public partial class GraphSystemService : IGraphCommentSystem + { + /// + /// Create new comment with rating + /// + /// GDID of autor node + /// GDID of target node + /// Scope for rating + /// Content + /// Byte array of data + /// State of publication + /// star 1-2-3-4-5 + /// Time of current action + /// ID + Comment IGraphCommentSystem.Create(GDID gAuthorNode, + GDID gTargetNode, + string dimension, + string content, + byte[] data, + PublicationState publicationState, + RatingValue value, + DateTime? timeStamp) + { + try + { + // 1. Get nodes for creation new comment + var authorNode = DoGetNode(gAuthorNode); + var targetNode = DoGetNode(gTargetNode); + var currentDateTime = timeStamp ?? App.TimeSource.UTCNow; + + if (!GraphHost.CanCreateComment(authorNode, targetNode, dimension, currentDateTime, value)) + throw new GraphException(StringConsts.GS_CAN_NOT_CREATE_COMMENT_ERROR.Args(authorNode, targetNode, value)); + + return DoCreate(targetNode, + authorNode, + null, dimension, + content, + data, + publicationState, + value, + currentDateTime); + } + catch (Exception ex) + { + Log(MessageType.Error, "GraphCommentSystem.Create()", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_CREATE_COMMENT_ERROR.Args(gTargetNode, gAuthorNode), ex); + } + } + + /// + /// Make response to target commentary + /// + /// Author + /// Parent commentary + /// Content of commentary + /// Byte Array + /// New CommentID + public Comment Respond(GDID gAuthorNode, CommentID parentId, string content, byte[] data) + { + try + { + if (parentId.IsZero) + throw new GraphException(StringConsts.GS_RESPONSE_BAD_PARENT_ID_ERROR); + + var currentDateTime = App.TimeSource.UTCNow; + + // Get parent comment + var parent = getCommentRow(parentId); + + if (parent == null) throw new GraphException(StringConsts.GS_COMMENT_NOT_FOUND.Args(parentId.G_Comment)); + if (!parent.IsRoot) throw new GraphException(StringConsts.GS_PARENT_ID_NOT_ROOT.Args(parentId.G_Comment)); + + var authorNode = GetNode(gAuthorNode); + var parentComment = GraphCommentFetchStrategy.RowToComment(parent); + + if (!GraphHost.CanCreateCommentResponse(parentComment, authorNode, currentDateTime)) + throw new GraphException(StringConsts.GS_CAN_NOT_CREATE_RESPONSE_ERROR.Args(authorNode, parentComment.TargetNode)); + + // Create new comment + return DoCreate(parentComment.TargetNode, + authorNode, + parentId, + parent.Dimension, + content, + data, + GSPublicationState.ToPublicationState(parent.PublicationState), + RatingValue.Undefined, + App.TimeSource.UTCNow); + } + catch (Exception ex) + { + Log(MessageType.Error, "GraphCommentSystem.Response", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_RESPONSE_COMMENT_ERROR.Args(parentId.G_Comment), ex); + } + } + + /// + /// Updates existing rating by ID + /// + public GraphChangeStatus Update(CommentID commentId, RatingValue value, string content, byte[] data) + { + if(commentId.IsZero) + return GraphChangeStatus.NotFound; + + try + { + // Taking comment row + var currentDateTime = App.TimeSource.UTCNow; + var ctxComment = ForComment(commentId.G_Volume); // CRUD context for comment + var comment = getCommentRow(commentId, ctxComment); + if (comment == null) + return GraphChangeStatus.NotFound; + + var targetNode = DoGetNode(comment.G_TargetNode); + if ((currentDateTime - comment.Create_Date) > GraphHost.EditCommentSpan(targetNode, comment.Dimension)) + return GraphChangeStatus.NotFound; + + var ctxNode = ForNode(comment.G_TargetNode); // CRUD context for target node + + // Updating fields + var filter = "Message,Data"; + comment.Message = content; + comment.Data = data; + + // if comment is root and rating value is not RatingValue.Undefined + // Update rating + if (comment.IsRoot && value != RatingValue.Undefined) + { + // Update NodeRating + var nodeRating = getNodeRating(comment.G_TargetNode, comment.Dimension, ctxNode); + nodeRating.UpdateRating((RatingValue) comment.Rating, -1); + nodeRating.UpdateRating(value, 1); + ctxNode.Update(nodeRating, filter: "Last_Change_Date,Cnt,Rating1,Rating2,Rating3,Rating4,Rating5".OnlyTheseFields()); + + // Update rating value of comment + comment.Rating = (byte) value; + filter = "Rating," + filter; + } + var resComment = ctxComment.Update(comment, filter: filter.OnlyTheseFields()); + return resComment == 0 ? GraphChangeStatus.NotFound : GraphChangeStatus.Updated; + } + catch (Exception ex) + { + Log(MessageType.Error, "GraphCommentSystem.Update", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_UPDATE_RATING_ERROR.Args(commentId.G_Comment), ex); + } + } + + /// + /// Delete comment + /// + /// Existing Comment ID + public GraphChangeStatus DeleteComment(CommentID commentId) + { + if(commentId.IsZero) + return GraphChangeStatus.NotFound; + + try + { + //1. Try get comment row + var ctxComment = ForComment(commentId.G_Volume); // CRUD context for comment + var comment = getCommentRow(commentId); + if (comment == null) + return GraphChangeStatus.NotFound; + + var ctxNode = ForNode(comment.G_TargetNode); // CRUD context for target node + + //2. Delete comment + comment.In_Use = false; + + if (comment.IsRoot) // Only Root comments has ratings and counts in comments of target node + { + //3. If comment has rating value, decrease value of that rating from target rating node + var nodeRating = getNodeRating(comment.G_TargetNode, comment.Dimension, ctxNode); + nodeRating.UpdateCount(-1); // Update count of comments + nodeRating.UpdateRating((RatingValue)comment.Rating, -1); // Update ratings (if RatingValue.Undefined - nothing happens) + nodeRating.Last_Change_Date = App.TimeSource.UTCNow; + ctxNode.Update(nodeRating, filter: "Last_Change_Date,Cnt,Rating1,Rating2,Rating3,Rating4,Rating5".OnlyTheseFields()); + } + // Update parent "ResponseCount" , if comment is not root + else if (comment.G_Parent.HasValue) + { + var parentCommendID = new CommentID(comment.G_CommentVolume, comment.G_Parent.Value); + var ctxParent = ForComment(parentCommendID.G_Volume); + var parent = getCommentRow(parentCommendID, ctxParent); + if (parent != null && parent.ResponseCount > 0) + { + parent.UpdateResponseCount(-1); + ctxParent.Update(parent, filter: "ResponseCount".OnlyTheseFields()); + } + } + + var res = ctxComment.Update(comment); + return res > 0 ? GraphChangeStatus.Deleted : GraphChangeStatus.NotFound; + } + catch (Exception ex) + { + Log(MessageType.Error, "DeleteComment", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_DELETE_COMMENT_ERROR.Args(commentId.G_Comment), ex); + } + } + + /// + /// Justify moderated comment + /// + /// Existing Comment ID + public GraphChangeStatus Justify(CommentID commentID) + { + if(commentID.IsZero) + return GraphChangeStatus.NotFound; + + try + { + var ctxComment = ForComment(commentID.G_Volume); + + var comment = getCommentRow(commentID, ctxComment); + if (comment == null) + return GraphChangeStatus.NotFound; + + var qry = Queries.CancelComplaintsByComment(commentID.G_Comment); + ctxComment.ExecuteWithoutFetch(qry); + + comment.ComplaintCount = 0; + ctxComment.Update(comment, filter: "ComplaintCount".OnlyTheseFields()); + return GraphChangeStatus.Updated; + } + catch (Exception ex) + { + Log(MessageType.Error, "Justify", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_JUSTIFY_COMMENT_ERROR.Args(commentID.G_Comment), ex); + } + } + + /// + /// Updates likes/dislikes + /// + public GraphChangeStatus Like(CommentID messageId, int deltaLike, int deltaDislike) + { + if(messageId.IsZero) return GraphChangeStatus.NotFound; + + try + { + var ctxRating = ForComment(messageId.G_Volume); + var qry = Queries.UpdateLike(messageId.G_Volume, messageId.G_Comment, deltaLike, deltaDislike); + var res = ctxRating.ExecuteWithoutFetch(qry); + return res==0 ? GraphChangeStatus.NotFound : GraphChangeStatus.Updated; + } + catch (Exception ex) + { + Log(MessageType.Error, "Like", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_SET_LIKE_ERROR.Args(messageId.G_Comment), ex); + } + } + + /// + /// Check if target node has existing comment, made by author + /// + /// Target node + /// Author + /// Scope of comments + public bool IsCommentedByAuthor(GDID gNode, GDID gAuthor, string dimension) + { + try + { + return DoIsCommentedByAuthor(gNode, gAuthor, dimension); + } + catch (Exception ex) + { + Log(MessageType.Error, "DoIsCommentedByAuthor", ex.ToMessageWithType(), ex); + throw new GraphException("IsCommentedByAuthor gNode={0} gAuthor={1} dimension={2}".Args(gNode, gAuthor, dimension), ex); + } + } + + public IEnumerable GetNodeSummaries(GDID gNode) + { + try + { + return DoGetNodeSummaries(gNode); + } + catch (Exception ex) + { + Log(MessageType.Error, "GetNodeSummaries", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_GET_RATING_ERROR.Args(gNode), ex); + } + } + + public IEnumerable Fetch(CommentQuery query) + { + try + { + return DoFetch(query); + } + catch (Exception ex) + { + Log(MessageType.Error, "Fetch", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_FETCH_RATING_ERROR.Args(query.G_TargetNode), ex); + } + } + + public IEnumerable FetchResponses(CommentID commentId) + { + try + { + return DoFetchResponses(commentId).ToArray(); + } + catch (Exception ex) + { + Log(MessageType.Error, "DoFetchResponses", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_FETCH_RESPONSE_ERROR.Args(commentId.G_Comment), ex); + } + } + + public IEnumerable FetchComplaints(CommentID commentID) + { + try + { + return DoFetchComplaints(commentID).ToArray(); + } + catch (Exception ex) + { + Log(MessageType.Error, "FetchComplaints", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_FETCH_COMPLAINTS_ERROR.Args(commentID.G_Comment), ex); + } + } + + public Comment GetComment(CommentID commentId) + { + try + { + return DoGetComment(commentId); + } + catch (Exception ex) + { + Log(MessageType.Error, "GetComment", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_GET_COMMENT_ERROR.Args(commentId.G_Comment), ex); + } + } + + public GraphChangeStatus Complain(CommentID commentId, GDID gAuthorNode, string kind, string message) + { + if(commentId.IsZero) return GraphChangeStatus.NotFound; + + try + { + var comment = getCommentRow(commentId); + if (comment == null) return GraphChangeStatus.NotFound; + + var ctx = ForComment(commentId.G_Volume); + var complaintRow = new ComplaintRow(true) + { + G_Comment = commentId.G_Comment, + G_AuthorNode = gAuthorNode, + Kind = kind, + Message = message, + Create_Date = App.TimeSource.UTCNow, + In_Use = true + }; + ctx.Insert(complaintRow); + + comment.ComplaintCount++; + ctx.Update(comment, filter: "ComplaintCount".OnlyTheseFields()); + + return GraphChangeStatus.Updated; + } + catch (Exception ex) + { + Log(MessageType.Error, "Complain", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_COMPLAINT_ERROR.Args(commentId.G_Comment), ex); + } + } + + #region Protected + + /// + /// Check if target node has existing comment, made by author + /// + /// Target node + /// Author + /// Scope of comments + protected virtual bool DoIsCommentedByAuthor(GDID gNode, GDID gAuthor, string dimension) + { + if (gNode.IsZero) throw new GraphException("gNode.IsZero=true"); + if (gAuthor.IsZero) throw new GraphException("gAuthor.IsZero=true"); + if (dimension.IsNullOrWhiteSpace()) throw new GraphException("dimension=null"); + + var ctxNode = ForNode(gNode); + var qryVolumes = Queries.FindCommentVolumes(gNode, dimension, MaxScanTailCommentVolumes, App.TimeSource.UTCNow); + var volumes = ctxNode.LoadEnumerable(qryVolumes); + var hasComments = volumes.Any(v => hasAnyComments(v.G_CommentVolume, gNode, gAuthor, dimension)); + + return hasComments; + } + + /// + /// Creating new comment + /// + /// Target node + /// Author node, who made comment + /// Parent comment id + /// Scope of comment + /// Content + /// Byte array of data + /// State of publication + /// star 1-2-3-4-5 + /// Time of current action + /// ID + protected virtual Comment DoCreate(GraphNode targetNode, + GraphNode authorNode, + CommentID? parentID, + string dimension, + string content, + byte[] data, + PublicationState publicationState, + RatingValue value, + DateTime? creationTime) + { + // Create comment and, if need, volume + var ctxNode = ForNode(targetNode.GDID); // get shard context for target node + var volume = parentID.HasValue // if we have parentID, we need to place response comment in same volume as parent + ? getVolume(targetNode.GDID, parentID.Value.G_Volume, ctxNode) + : getEmptyVolume(targetNode.GDID, dimension, ctxNode); + + if(volume == null) + throw new GraphException(StringConsts.GS_RESPONSE_VOLUME_MISSING_ERROR); + + var comment = new CommentRow(true) + { + G_CommentVolume = volume.G_CommentVolume, + G_Parent = parentID.HasValue ? parentID.Value.G_Comment : (GDID?)null, + G_AuthorNode = authorNode.GDID, + G_TargetNode = targetNode.GDID, + Create_Date = creationTime ?? App.TimeSource.UTCNow, + Dimension = dimension, + + Message = content, + Data = data, + PublicationState = GSPublicationState.ToDomainString(publicationState), + Rating = (byte)value, + + Like = 0, + Dislike = 0, + ComplaintCount = 0, + ResponseCount = 0, + + IsRoot = !parentID.HasValue, + In_Use = true + }; + ForComment(volume.G_CommentVolume).Insert(comment); + + // Update ratings , if comment is root + if (comment.IsRoot) + { + var nodeRating = getNodeRating(targetNode.GDID, dimension, ctxNode); + nodeRating.UpdateCount(1); // Update count of commentaries (only root commentaries counts) + nodeRating.UpdateRating(value, 1); //Update ratings + nodeRating.Last_Change_Date = App.TimeSource.UTCNow; // update change time + ctxNode.Update(nodeRating, filter: "Last_Change_Date,Cnt,Rating1,Rating2,Rating3,Rating4,Rating5".OnlyTheseFields()); + } + // Update parent ResponseCount, if comment is not root + else if(parentID.HasValue) + { + var ctxParent = ForComment(parentID.Value.G_Volume); + var parent = getCommentRow(parentID.Value, ctxParent); + if (parent != null) + { + parent.UpdateResponseCount(1); + ctxParent.Update(parent, filter: "ResponseCount".OnlyTheseFields()); + } + } + + // Update count of commentaries in volume + volume.Count++; + ctxNode.Update(volume, filter: "Count".OnlyTheseFields()); + return Comment.MakeNew(new CommentID(comment.G_CommentVolume, comment.GDID), + parentID, + authorNode, + targetNode, + comment.Create_Date, + comment.Dimension, + GSPublicationState.ToPublicationState(comment.PublicationState), + (RatingValue)comment.Rating, + comment.Message, + comment.Data); + } + + protected virtual IEnumerable DoGetNodeSummaries(GDID gNode) + { + var qry = Queries.FindNodeRatings(gNode, App.TimeSource.UTCNow); + var rows = ForNode(gNode).LoadEnumerable(qry); + return rows.Select(row => new SummaryRating(row.G_Node, + row.Create_Date, + row.Last_Change_Date, + row.Dimension, + row.Cnt, + row.Rating1, + row.Rating2, + row.Rating3, + row.Rating4, + row.Rating5)).ToArray(); + } + + protected virtual IEnumerable DoFetch(CommentQuery query) + { + return GraphCommentFetchStrategy.Fetch(query); + } + + protected virtual IEnumerable DoFetchResponses(CommentID commentId, int offset = 0, int limit = 1) // todo доделать + { + var qryResponses = Queries.FindResponses(commentId); + var responses = ForComment(commentId.G_Volume).LoadEnumerable(qryResponses); + return responses.Where(r => r.In_Use || (!r.In_Use && r.ResponseCount > 0)).Select(GraphCommentFetchStrategy.RowToComment); + } + + protected IEnumerable DoFetchComplaints(CommentID commentID) + { + var qryComplaints = Queries.FindComplaints(commentID.G_Comment); + var complaints = ForComment(commentID.G_Volume).LoadEnumerable(qryComplaints); + return complaints.Select(c => rowToComplaint(commentID, c)); + } + + protected CRUDOperations ForComment(GDID gVolume) + { + return DataStore.PartitionedOperationsFor(SocialConsts.MDB_AREA_COMMENT, gVolume); + } + + #endregion + + #region .pvt + + /// + /// Get existing not full Comment Volume or create a new one + /// + /// TargetNode context + /// TargetNode GDID + /// Scope of comment + /// Existing or new volume + private CommentVolumeRow getEmptyVolume(GDID gTargetNode, string dimension, CRUDOperations? ctxNode = null) + { + var ctx = ctxNode ?? ForNode(gTargetNode); + var qryVolume = Queries.FindEmptyCommentVolume(gTargetNode, dimension, MaxSizeCommentVolume); + var volume = ctxNode.LoadRow(qryVolume); + if (volume == null) + { + volume = new CommentVolumeRow + { + G_Owner = gTargetNode, + G_CommentVolume = NodeRow.GenerateNewNodeRowGDID(), + Dimension = dimension, + Count = 0, + Create_Date = App.TimeSource.UTCNow + }; + ctx.Insert(volume); + } + return volume; + } + + /// + /// Get existing CommentVolume by TargetNode and Volume + /// + /// TargetNode context + /// TargetNode GDID + /// Volume GDID + /// Existing volume + private CommentVolumeRow getVolume(GDID gTargetNode, GDID gVolume, CRUDOperations? ctxNode = null) + { + var ctx = ctxNode ?? ForNode(gTargetNode); + var qryVolume = Queries.FindCommentVolume(gTargetNode, gVolume); + return ctx.LoadRow(qryVolume); + } + + /// + /// Get existing NodeRating or create a new one + /// + /// TargetNode context + /// TargetNode GDID + /// Scope of comment + /// Existing or new NodeRating + private NodeRatingRow getNodeRating(GDID gTargetNode, string dimension, CRUDOperations? ctxNode = null) + { + var ctx = ctxNode ?? ForNode(gTargetNode); + var qryNodeRating = Queries.FindNodeRating(gTargetNode, dimension); + var nodeRating = ctxNode.LoadRow(qryNodeRating); + if(nodeRating == null) + { + nodeRating = new NodeRatingRow + { + G_Node = gTargetNode, + Create_Date = App.TimeSource.UTCNow, + Last_Change_Date = App.TimeSource.UTCNow, + Dimension = dimension, + Rating1 = 0, + Rating2 = 0, + Rating3 = 0, + Rating4 = 0, + Rating5 = 0, + Cnt = 0 + }; + ctx.Insert(nodeRating); + } + return nodeRating; + } + + private Comment DoGetComment(CommentID commentId) + { + var qry = Queries.FindCommentByGDID(commentId.G_Comment); + var row = ForComment(commentId.G_Volume).LoadRow(qry); + if (row == null) throw new GraphException("Comment not found, CommendID = {0}".Args(commentId)); + return GraphCommentFetchStrategy.RowToComment(row); + } + + private CommentRow getCommentRow(CommentID commentId, CRUDOperations? ctxComment = null) + { + if (commentId.IsZero) return null; + var ctx = ctxComment ?? ForComment(commentId.G_Volume); + var qryComment = Queries.FindCommentByGDID(commentId.G_Comment); + return ctx.LoadRow(qryComment); + } + + private bool hasAnyComments(GDID gVolume, GDID gNode, GDID gAuthor, string dimension) + { + var qryHasComments = Queries.HasCommentsCreatedByAuthor(gNode, gAuthor, dimension); + var row = ForComment(gVolume).LoadOneRow(qryHasComments); + return row != null; + } + + private Complaint rowToComplaint(CommentID commentID, ComplaintRow row) + { + if (row == null) return default(Complaint); + + return new Complaint(new CommentID(commentID.G_Volume, row.G_Comment), + row.GDID, + GetNode(row.G_AuthorNode), + row.Kind, + row.Message, + row.Create_Date, + row.In_Use); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/GraphSystemService.Event.cs b/src/Agni.Social/Graph/Server/GraphSystemService.Event.cs new file mode 100644 index 0000000..5d70210 --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphSystemService.Event.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Environment; +using NFX.Log; +using NFX.Serialization.JSON; + +using Agni.Social.Graph.Server.Data; +using Agni.Social.Graph.Server.Workers; +using Agni.Workers; + +namespace Agni.Social.Graph.Server +{ + public partial class GraphSystemService + { + /// + /// Emits the event - notifies all subscribers (watchers, friends etc.) about the event. + /// The physical notification happens via IGraphHost implementation + /// + void IGraphEventSystem.EmitEvent(Event evt) + { + try + { + IConfigSectionNode cfg = null; + if (evt.Config.IsNotNullOrWhiteSpace()) cfg = evt.Config.AsLaconicConfig(handling: ConvertErrorHandling.Throw); + DoEmitEvent(evt); + } + catch (Exception ex) + { + Log(MessageType.Error, "EmitEvent", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_EMIT_EVENT_ERROR.Args(evt.ToJSON()), ex); + } + } + + /// + /// Subscribes recipient node to the emitter node. Unlike friends the susbscription connection is uni-directional + /// + void IGraphEventSystem.Subscribe(GDID gRecipientNode, GDID gEmitterNode, byte[] parameters) + { + try + { + DoSubscribe(gRecipientNode, gEmitterNode, parameters); + } + catch (Exception ex) + { + Log(MessageType.Error, "Subscribe", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_SUBSCRIBE_ERROR.Args(gRecipientNode.ToString(), gEmitterNode.ToString()), ex); + } + } + + /// + /// Removes the subscription. Unlike friends the subscription connection is uni-directional + /// + void IGraphEventSystem.Unsubscribe(GDID gRecipientNode, GDID gEmitterNode) + { + try + { + DoUnsubscribe(gRecipientNode, gEmitterNode); + } + catch (Exception ex) + { + Log(MessageType.Error, "DoUnsubscribe", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_UNSUBSCRIBE_ERROR.Args(gRecipientNode.ToString(), gEmitterNode.ToString()), ex); + } + } + + /// + /// Returns an estimated approximate number of subscribers that an emitter has + /// + long IGraphEventSystem.EstimateSubscriberCount(GDID gEmitterNode) + { + try + { + return DoEstimateSubscriberCount(gEmitterNode); + } + catch (Exception ex) + { + Log(MessageType.Error,"EstimateSubscriberCount", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_ESTIMATE_SUBSCRIPTION_COUNT_ERROR.Args(gEmitterNode.ToString()), ex); + } + } + + /// + /// Returns Subscribers for Emitter from start position + /// + IEnumerable IGraphEventSystem.GetSubscribers(GDID gEmitterNode, long start, int count) + { + try + { + return DoGetSubscribers(gEmitterNode, start, count); + } + catch (Exception ex) + { + Log(MessageType.Error,"GetSubscribers", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_GET_SUBSCRIBER_ERROR.Args(gEmitterNode.ToString()), ex); + } + } + + protected virtual void DoEmitEvent(Event evt) + { + DoGetNode(evt.G_EmitterNode); // Don't run todo if Emitter does not exist + var qryVol = Queries.CountSubscriberVolumes(evt.G_EmitterNode); + var countVol = ForNode(evt.G_EmitterNode).LoadRow(qryVol)["CNT"].AsInt(); + var count = countVol < EventDeliveryCohortSize ? countVol : EventDeliveryCohortSize; + var todos = new List(); + for(int i=0; i < count; i++) + { + var todo = Todo.MakeNew(); + todo.Event = evt; + todo.VolumeWorkerOffset = count; + todo.VolumeIndex = i; + todo.ChunkIndex = 0; + todo.G_Volume = GDID.Zero; + todos.Add(todo); + } + SocialGraphTodos.EnqueueSubscribtion(todos); + } + + protected virtual void DoSubscribe(GDID gRecipientNode, GDID gEmitterNode, byte[] parameters) + { + var emitter = DoGetNode(gEmitterNode); + var gh = DoGetNode(gRecipientNode); + if (!GraphHost.CanBeSubscribed(gh.NodeType, emitter.NodeType)) throw new GraphException(StringConsts.GS_CAN_NOT_BE_SUSBCRIBED_ERROR.Args(gh.NodeType, emitter.NodeType)); + var todo = Todo.MakeNew(); + todo.G_Owner = gEmitterNode; + todo.G_Subscriber = gRecipientNode; + todo.Subs_Type = gh.NodeType; + todo.Parameters = parameters; + SocialGraphTodos.EnqueueSubscribtion(todo); + } + + protected virtual void DoUnsubscribe(GDID gRecipientNode, GDID gEmitterNode) + { + var todo =Todo.MakeNew(); + todo.G_Owner = gEmitterNode; + todo.G_Subscriber = gRecipientNode; + SocialGraphTodos.EnqueueSubscribtion(todo); + } + + protected virtual long DoEstimateSubscriberCount(GDID gEmitterNode) + { + var count = ForNode(gEmitterNode).LoadOneRow(Queries.CountSubscribers(gEmitterNode)); + return count[0].AsLong(); + } + + protected virtual IEnumerable DoGetSubscribers(GDID gEmitterNode, long start, int count) + { + var qryVol = Queries.FindSubscriberVolumes(gEmitterNode); + IEnumerable volumes = ForNode(gEmitterNode).LoadEnumerable(qryVol); + var sum = 0; + SubscriberVolumeRow volume = null; + var i = 0; + foreach (var vol in volumes) + { + if (sum <= start && start <= sum + SocialConsts.GetVolumeMaxCountForPosition(i++)) + { + volume = vol; + break; + } + sum += vol.Count; + } + if(volume == null) yield break; + var _start = start - sum; + var qry = Queries.FindSubscribers(volume.G_SubscriberVolume, _start, count); + var subscribers = ForNode(volume.G_SubscriberVolume).LoadEnumerable(qry); + foreach (var subscriber in subscribers) + { + yield return DoGetNode(subscriber.G_Subscriber); + } + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/GraphSystemService.Friend.cs b/src/Agni.Social/Graph/Server/GraphSystemService.Friend.cs new file mode 100644 index 0000000..c96a9f1 --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphSystemService.Friend.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Log; + +using Agni.Social.Graph.Server.Data; +using Agni.Social.Graph.Server.Data.Schema; + + +namespace Agni.Social.Graph.Server +{ + public partial class GraphSystemService + { + /// + /// Returns an enumeration of friend list ids for the particular node + /// + IEnumerable IGraphFriendSystem.GetFriendLists(GDID gNode) + { + try + { + return DoGetFriendLists(gNode); + } + catch (Exception ex) + { + Log(MessageType.Error, "GetFriendLists", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_GET_FRIEND_LISTS_ERROR.Args(gNode.ToString()), ex); + } + } + + /// + /// Adds a new friend list id for the particular node. The list id may not contain commas + /// + GraphChangeStatus IGraphFriendSystem.AddFriendList(GDID gNode, string list, string description) + { + try + { + return DoAddFriendList(gNode, list, description); + } + catch (Exception ex) + { + Log(MessageType.Error, "AddFriendList", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_ADD_FRIEND_LIST_ERROR.Args(gNode.ToString()), ex); + } + } + + /// + /// Removes friend list id for the particular node + /// + GraphChangeStatus IGraphFriendSystem.DeleteFriendList(GDID gNode, string list) + { + try + { + return DoDeleteFriendList(gNode, list); + } + catch (Exception ex) + { + Log(MessageType.Error, "DeleteFriendList", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_DELETE_FRIEND_LIST_ERROR.Args(gNode.ToString()), ex); + } + } + + /// + /// Returns an enumeration of FriendConnection{GraphNode, approve date, direction, groups} + /// + IEnumerable IGraphFriendSystem.GetFriendConnections(FriendQuery query) + { + try + { + return DoGetFriendConnections(query); + } + catch (Exception ex) + { + Log(MessageType.Error, "GetFriendConnections", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_GET_FRIEND_CONNECTIONS_ERROR.Args(query.ToString()), ex); + } + } + + /// + /// Adds a bidirectional friend connection between gNode and gFriendNode + /// If friend connection already exists updates the approve/ban stamp by the receiving party (otherwise approve is ignored) + /// If approve==null then no stamps are set, if true connection is approved given that gNode is not the one who initiated the connection, + /// false then connection is banned given that gNode is not the one who initiated the connection + /// + GraphChangeStatus IGraphFriendSystem.AddFriend(GDID gNode, GDID gFriendNode, bool? approve) + { + try + { + return DoAddFriend(gNode, gFriendNode, approve); + } + catch (Exception ex) + { + Log(MessageType.Error, "AddFriend", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_ADD_FRIEND_ERROR.Args(gNode.ToString(), gFriendNode.ToString()), ex); + } + } + + /// + /// Assigns lists to the gNode (the operation is unidirectional - it only assigns the lists on the gNode). + /// Lists is a comma-separated list of friend list ids + /// + GraphChangeStatus IGraphFriendSystem.AssignFriendLists(GDID gNode, GDID gFriendNode, string lists) + { + try + { + return DoAssignFriendLists(gNode, gFriendNode, lists); + } + catch (Exception ex) + { + Log(MessageType.Error, "AssignFriendLists", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_ASSIGN_FRIEND_LISTS_ERROR.Args(gNode.ToString(), gFriendNode.ToString()), ex); + } + } + + /// + /// Deletes friend connections. The operation drops both connections from node and friend + /// + GraphChangeStatus IGraphFriendSystem.DeleteFriend(GDID gNode, GDID gFriendNode) + { + try + { + return DoDeleteFriend(gNode, gFriendNode); + } + catch (Exception ex) + { + Log(MessageType.Error, "DeleteFriend", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_DELETE_FRIEND_ERROR.Args(gNode.ToString(), gFriendNode.ToString()), ex); + } + } + + protected virtual IEnumerable DoGetFriendLists(GDID gNode) + { + DoGetNode(gNode); + var rows = ForNode(gNode).LoadEnumerable(Queries.FindFriendListByNode(gNode)); + foreach (var row in rows) + { + yield return row.List_ID; + } + } + + protected virtual GraphChangeStatus DoAddFriendList(GDID gNode, string list, string description) + { + DoGetNode(gNode); + var row = new FriendListRow(true) + { + G_Owner = gNode, + List_ID = list, + List_Description = description, + Create_Date = App.TimeSource.UTCNow + }; + ForNode(gNode).Insert(row); + return GraphChangeStatus.Added; + } + + protected virtual GraphChangeStatus DoDeleteFriendList(GDID gNode, string list) + { + ForNode(gNode).ExecuteWithoutFetch(Queries.DeleteFriendListByListId(gNode, list)); + return GraphChangeStatus.Deleted; + } + + protected virtual IEnumerable DoGetFriendConnections(FriendQuery query, ICacheParams cacheParams = null) + { + var rows = ForNode(query.G_Node).LoadEnumerable(Queries.FindFriends(query)); + foreach (var row in rows) + { + var friendNode = DoGetNode(row.G_Friend, cacheParams); + foreach (var graphNode in GraphHost.FilterByOriginQuery(new[] {friendNode}, query.OriginQuery)) + { + yield return new FriendConnection(graphNode, + row.Request_Date, + FriendStatus.Approved.Equals(GSFriendStatus.ToFriendStatus(row.Status)) + ? (DateTime?) row.Status_Date + : null, + GSFriendshipRequestDirection.ToFriendshipRequestDirection(row.Direction), + GSFriendVisibility.ToFriendVisibility(row.Visibility), + row.Lists); + } + } + } + + protected virtual GraphChangeStatus DoAddFriend(GDID gNode, GDID gFriendNode, bool? approve) + { + var node = DoGetNode(gNode); + var friendNode = DoGetNode(gFriendNode); + + if(!GraphHost.CanBeFriends(node.NodeType, friendNode.NodeType)) throw new GraphException(StringConsts.GS_FRIEND_DRIECTION_ERROR.Args(node.NodeType, friendNode.NodeType)); + + int countMe = countFriends(gNode); + int countFriend = countFriends(gFriendNode); + + if (countMe > SocialConsts.GS_MAX_FRIENDS_COUNT) throw new GraphException(StringConsts.GS_MAX_FRIENDS_IN_NODE.Args(gNode)); + if (countFriend > SocialConsts.GS_MAX_FRIENDS_COUNT) throw new GraphException(StringConsts.GS_MAX_FRIENDS_IN_NODE.Args(gFriendNode)); + + + GraphChangeStatus resultMe = addFriend(gNode, gFriendNode, approve, FriendshipRequestDirection.I); + GraphChangeStatus resultFriend = addFriend(gFriendNode, gNode, approve, FriendshipRequestDirection.Friend); + + return resultMe == GraphChangeStatus.Added && resultFriend == GraphChangeStatus.Added ? GraphChangeStatus.Added : GraphChangeStatus.Updated; + } + + protected virtual GraphChangeStatus DoAssignFriendLists(GDID gNode, GDID gFriendNode, string lists) + { + DoGetNode(gNode); + DoGetNode(gFriendNode); + + var ctx = ForNode(gNode); + var row = ctx.LoadRow(Queries.FindOneFriendByNodeAndFriend(gNode, gFriendNode)); + if (row == null) return GraphChangeStatus.NotFound; + row.Lists = addToList(row.Lists, lists); + ctx.Update(row); + return GraphChangeStatus.Updated; + } + + protected virtual GraphChangeStatus DoDeleteFriend(GDID gNode, GDID gFriendNode) + { + ForNode(gNode).ExecuteWithoutFetch(Queries.DeleteFriendByNodeAndFriend(gNode, gFriendNode)); + ForNode(gFriendNode).ExecuteWithoutFetch(Queries.DeleteFriendByNodeAndFriend(gFriendNode, gNode)); + return GraphChangeStatus.Deleted; + } + + + private GraphChangeStatus addFriend(GDID gNode, GDID gFriendNode, bool? approve, FriendshipRequestDirection direction) + { + GraphChangeStatus result; + var ctx = ForNode(gNode); + var row = ctx.LoadRow(Queries.FindOneFriendByNodeAndFriend(gNode, gFriendNode)); + if (row != null) + { + if (approve == null) return GraphChangeStatus.Updated; + row.Status = approve.Value ? GSFriendStatus.APPROVED : GSFriendStatus.DENIED ; + row.Status_Date = App.TimeSource.UTCNow; + ctx.Update(row); + result = GraphChangeStatus.Updated; + } + else + { + row = new FriendRow(true) + { + G_Owner = gNode, + G_Friend = gFriendNode, + Status_Date = App.TimeSource.UTCNow, + Status = approve!= null && approve.Value ? GSFriendStatus.APPROVED: GSFriendStatus.PENDING, + Visibility = GSFriendVisibility.FRIENDS, + Request_Date = App.TimeSource.UTCNow, + Direction = GSFriendshipRequestDirection.ToDomainString(direction) + }; + ctx.Insert(row); + result = GraphChangeStatus.Added; + } + return result; + } + + private string addToList(string rowLists, string lists) + { + HashSet oldList = new HashSet(rowLists.Split(SocialConsts.GS_FRIEND_LIST_SEPARATOR.ToCharArray())); + lists.Split(SocialConsts.GS_FRIEND_LIST_SEPARATOR.ToCharArray()).ForEach(s => oldList.Add(s)); + return string.Join(SocialConsts.GS_FRIEND_LIST_SEPARATOR, oldList); + } + + private int countFriends(GDID gNode) + { + var count = ForNode(gNode).LoadOneRow(Queries.CountFriends(gNode)); + return count[0].AsInt(); + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/GraphSystemService.Node.cs b/src/Agni.Social/Graph/Server/GraphSystemService.Node.cs new file mode 100644 index 0000000..00afe04 --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphSystemService.Node.cs @@ -0,0 +1,191 @@ +using System; + +using NFX; +using NFX.ApplicationModel.Pile; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Log; +using NFX.Serialization.JSON; + +using Agni.Social.Graph.Server.Data; +using Agni.Social.Graph.Server.Data.Schema; +using Agni.Social.Graph.Server.Workers; + +namespace Agni.Social.Graph.Server +{ + public partial class GraphSystemService + { + /// + /// Saves the GraphNode instances into the system. + /// If a node with such ID already exists, updates it, otherwise creates new node + /// + public GraphChangeStatus SaveNode(GraphNode node) + { + try + { + return DoSaveNode(node); + } + catch (Exception ex) + { + Log(MessageType.Error, "SaveNode", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_SAVE_NODE_ERROR.Args(node.ToJSON()), ex); + } + } + + /// + /// Fetches the GraphNode by its unique GDID or null if not found + /// + public GraphNode GetNode(GDID gNode) + { + try + { + return DoGetNode(gNode); + } + catch (Exception ex) + { + Log(MessageType.Error, "GetNode", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_GET_NODE_ERROR.Args(gNode.ToString()), ex); + } + } + + /// + /// Deletes node by GDID + /// + public GraphChangeStatus DeleteNode(GDID gNode) + { + try + { + return DoDeleteNode(gNode); + } + catch (Exception ex) + { + Log(MessageType.Error, "DeleteNode", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_DELETE_NODE_ERROR.Args(gNode.ToString()), ex); + } + } + + /// + /// Undeletes node by GDID + /// + public GraphChangeStatus UndeleteNode(GDID gNode) + { + try + { + return DoUndeleteNode(gNode); + } + catch (Exception ex) + { + Log(MessageType.Error, "UndeleteNode", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_DELETE_NODE_ERROR.Args(gNode.ToString()), ex); + } + } + + /// + /// Remove node by GDID from Databases + /// + public GraphChangeStatus RemoveNode(GDID gNode) + { + try + { + return DoRemoveNode(gNode); + } + catch (Exception ex) + { + Log(MessageType.Error, "RemoveNode", ex.ToMessageWithType(), ex); + throw new GraphException(StringConsts.GS_DELETE_NODE_ERROR.Args(gNode.ToString()), ex); + } + } + + + protected virtual GraphChangeStatus DoRemoveNode(GDID gNode) + { + var removeFriendsTodo = new EventRemoveFriendsTodo() + { + G_Node = gNode, + FriendIndex = 0, + G_Friend = GDID.Zero + }; + SocialGraphTodos.EnqueueRemove(removeFriendsTodo); + + var removeNodeTodo = new EventRemoveNodeTodo() + { + G_Node = gNode, + VolumeIndex = 0, + G_Volume = GDID.Zero + }; + SocialGraphTodos.EnqueueRemove(removeNodeTodo); + + return GraphChangeStatus.Unassigned; + } + + private GraphChangeStatus DoUndeleteNode(GDID gNode) + { + var qry = Queries.ChangeInUseNodeByGDID(gNode, isDel:false); + var affected = ForNode(gNode).ExecuteWithoutFetch(qry); + return (affected>0) ? GraphChangeStatus.Updated : GraphChangeStatus.NotFound; + } + + protected virtual GraphChangeStatus DoSaveNode(GraphNode node) + { + GraphChangeStatus result; + + var row = loadNodeRow(node.GDID); + if (row == null) + { + row = new NodeRow(false) + { + GDID = node.GDID, + In_Use = true, + Node_Type = node.NodeType, + G_OriginShard = node.G_OriginShard, + G_Origin = node.G_Origin, + Create_Date = node.TimestampUTC + }; + result = GraphChangeStatus.Added; + } + else + result = GraphChangeStatus.Updated; + + row.Origin_Name = node.OriginName; + row.Friend_Visibility = GSFriendVisibility.ToDomainString(node.DefaultFriendVisibility); + + ForNode(node.GDID).Upsert(row); + + return result; + } + + protected virtual GraphNode DoGetNode(GDID gNode, ICacheParams cacheParams = null) + { + var row = Cache.FetchThrough(gNode, + SocialConsts.GS_NODE_TBL, + cacheParams, + gdid => loadNodeRow(gNode)); + + if (row == null) return new GraphNode(); + + return new GraphNode(row.Node_Type, + row.GDID, + row.G_OriginShard, + row.G_Origin, + row.Origin_Name, + row.Origin_Data, + row.Create_Date.Value, + GSFriendVisibility.ToFriendVisibility(row.Friend_Visibility)); + } + + protected virtual GraphChangeStatus DoDeleteNode(GDID gNode) + { + var qry = Queries.ChangeInUseNodeByGDID(gNode, true); + var affected = ForNode(gNode).ExecuteWithoutFetch(qry); + return (affected>0) ? GraphChangeStatus.Deleted : GraphChangeStatus.NotFound; + } + + + + private NodeRow loadNodeRow(GDID gNode) + { + return ForNode(gNode).LoadRow(Queries.FindOneNodeByGDID(gNode)); + } + + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/GraphSystemService.cs b/src/Agni.Social/Graph/Server/GraphSystemService.cs new file mode 100644 index 0000000..df5917c --- /dev/null +++ b/src/Agni.Social/Graph/Server/GraphSystemService.cs @@ -0,0 +1,157 @@ +using System; + +using NFX; +using NFX.ApplicationModel.Pile; +using NFX.DataAccess; +using NFX.DataAccess.Distributed; +using NFX.Environment; +using NFX.Log; +using NFX.ServiceModel; + +using Agni.MDB; +using Agni.WebMessaging; + +namespace Agni.Social.Graph.Server +{ + public partial class GraphSystemService : ServiceWithInstrumentationBase + , IGraphEventSystem + , IGraphFriendSystem + , IGraphNodeSystem + , IGraphCommentSystem + { + #region CONSTS + + public const int DEFAULT_EVENT_DELIVERY_COHORT_SIZE = 32; + public const int MAX_SIZE_COMMENT_VOLUME = 1000; + public const int MAX_SCAN_TAIL_COMMENT_VOLUMES = 2; + + #endregion + + #region STATIC/.ctor + private static object s_Lock = new object(); + private static volatile GraphSystemService s_Instance; + + + internal static GraphSystemService Instance + { + get + { + var instance = s_Instance; + if (instance == null) + throw new GraphException(StringConsts.GS_INSTANCE_NOT_ALLOCATED_ERROR.Args(typeof(GraphSystemService).Name)); + return instance; + } + } + + public GraphSystemService(object director) : base(director) + { + lock (s_Lock) + { + if (s_Instance != null) + throw new WebMessagingException(StringConsts.GS_INSTANCE_ALREADY_ALLOCATED_ERROR.Args(GetType().Name)); + s_Instance = this; + } + } + + protected override void Destructor() + { + lock (s_Lock) + { + base.Destructor(); + s_Instance = null; + } + } + + #endregion + + #region fields + + private GraphCommentFetchDefaultStrategy m_GraphCommentFetchStrategy; + + #endregion + + #region properties + + [Config, ExternalParameter(CoreConsts.EXT_PARAM_GROUP_INSTRUMENTATION)] + public override bool InstrumentationEnabled { get; set; } + + internal GraphCommentFetchDefaultStrategy GraphCommentFetchStrategy { get { return m_GraphCommentFetchStrategy; } } + + internal GraphHost GraphHost { get { return GraphOperationContext.Instance.GraphHost; }} + internal IMDBDataStore DataStore { get { return GraphOperationContext.Instance.DataStore; }} + internal ICache Cache { get { return DataStore.Cache;}} + + /// + /// Defines the number of EventDeliveryTodo instances executing in parallel + /// + internal int EventDeliveryCohortSize { get { return DEFAULT_EVENT_DELIVERY_COHORT_SIZE;} } + + /// + /// Sets the limit of the number of comments per comment volume + /// + internal int MaxSizeCommentVolume { get { return MAX_SIZE_COMMENT_VOLUME; }} + + /// + /// Specifies how many tail comment volumes should be scanned for detection of duplicates + /// + internal int MaxScanTailCommentVolumes { get { return MAX_SCAN_TAIL_COMMENT_VOLUMES; }} + + #endregion + + #region public + + public CRUDOperations ForNode(GDID gNode) + { + return DataStore.PartitionedOperationsFor(SocialConsts.MDB_AREA_NODE, gNode); + } + + #endregion + + #region protected + + protected override void DoConfigure(IConfigSectionNode node) + { + base.DoConfigure(node); + + if (node == null || !node.Exists) return; + + var nGraphComment = node[SocialConsts.CONFIG_GRAPH_COMMENT_FETCH_STRATEGY_SECTION]; + if (!nGraphComment.Exists) throw new SocialException(StringConsts.GS_INIT_NOT_CONF_ERRROR.Args(this.GetType().Name, SocialConsts.CONFIG_GRAPH_COMMENT_FETCH_STRATEGY_SECTION)); + m_GraphCommentFetchStrategy = FactoryUtils.MakeAndConfigure(nGraphComment, args: new object[] { this, nGraphComment }); + + } + + protected override void DoStart() + { + base.DoStart(); + } + + protected override void DoSignalStop() + { + base.DoSignalStop(); + } + + protected override void DoWaitForCompleteStop() + { + base.DoWaitForCompleteStop(); + } + + protected void Log(MessageType type, string from, string text, Exception error = null, Guid? related = null) + { + var msg = new NFX.Log.Message + { + Type = type, + Topic = SysConsts.LOG_TOPIC_APP_MANAGEMENT, + From = "{0}.{1}".Args(GetType().FullName, from), + Text = text, + Exception = error + }; + + if (related.HasValue) msg.RelatedTo = related.Value; + + App.Log.Write( msg ); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Workers/EventDeliverTodo.cs b/src/Agni.Social/Graph/Server/Workers/EventDeliverTodo.cs new file mode 100644 index 0000000..e481678 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Workers/EventDeliverTodo.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Resources; +using Agni.Coordination; +using Agni.MDB; +using Agni.Social.Graph.Server.Data; +using Agni.Workers; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Environment; +using NFX.Log; +using NFX.Serialization.Arow; + +namespace Agni.Social.Graph.Server.Workers +{ + + [Arow] + [TodoQueue(SocialConsts.TQ_EVT_SUB_DELIVER, "A1257BDA-B366-43CF-85FD-D323A11091BD")] + public sealed class EventDeliverTodo : EventDeliveryBase + { + /// + /// Смещение по итерациям + /// + [Field(backendName: "vol_vwo")] public int VolumeWorkerOffset { get; set; } + /// + /// Увеличивается на OffsetSize по завершении обработки конкретного volume. + /// При следующей итерации индекс последующего volume вычисляется по формуле : VolumeIteration + OffsetSize + /// + [Field(backendName: "vol_idx")] public int VolumeIndex { get; set; } + /// + /// Это номер куска (чанка) работы производимой внутри данного volume + /// + [Field(backendName: "chk_idx")] public int ChunkIndex { get; set; } + /// + /// Current volume subscribers + /// + [Field(backendName: "g_vol")] public GDID G_Volume { get; set; } + + protected override void DoPrepareForEnqueuePostValidate(string targetName) + { + base.DoPrepareForEnqueuePostValidate(targetName); + var key = G_Volume.ToString(); + SysShardingKey = key; + SysParallelKey = key; + } + + protected override ExecuteState Execute(ITodoHost host, DateTime utcBatchNow) + { + try + { + if (G_Volume.IsZero) G_Volume = getVolume(); + var subscribers = readNextRows(); + var count = subscribers.Count(); + sendEventsChunk(subscribers); + if (count < SocialConsts.SUBSCRIPTION_DELIVERY_CHUNK_SIZE) + { + G_Volume = getNextVolume(); + if (G_Volume.IsZero) return ExecuteState.Complete; + ChunkIndex = 0; + } + else + { + ChunkIndex += count; + } + return ExecuteState.ReexecuteUpdated; + } + catch (Exception error) + { + host.Log(MessageType.Error, this, "Deliver()", error.ToMessageWithType(), error); + } + return ExecuteState.ReexecuteAfterError; + } + + protected override int RetryAfterErrorInMs(DateTime utcBatchNow) + { + if (SysTries < 2) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(1000, 3000); + if (SysTries < 5) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(20000, 60000); + if (SysTries <= 10) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(80000, 160000); + throw new GraphException("Event deliver after failure {0} retries".Args(10)); + } + + private GDID getNextVolume() + { + VolumeIndex = VolumeIndex + VolumeWorkerOffset; + return getVolume(); + } + + private GDID getVolume() + { + var qry = Queries.GetNextVolume(G_Emitter, VolumeIndex); + var row = ForNode(G_Emitter).LoadRow(qry); + return row != null ? row.G_SubscriberVolume : GDID.Zero; + } + + private IEnumerable readNextRows() + { + var qry = Queries.FindSubscribers(G_Volume, ChunkIndex, + SocialConsts.SUBSCRIPTION_DELIVERY_CHUNK_SIZE); + return ForNode(G_Volume).LoadEnumerable(qry); + } + + private void sendEventsChunk(IEnumerable subscribersChunk) + { + var host = GraphOperationContext.Instance.GraphHost; + IConfigSectionNode cfg = null; + if (Event.Config.IsNotNullOrWhiteSpace()) cfg = Event.Config.AsLaconicConfig(handling: ConvertErrorHandling.ReturnDefault); + + var filtered = host.FilterEventsChunk(subscribersChunk, Event, cfg); + var badSubs = host.DeliverEventsChunk(filtered, Event, cfg); + if (badSubs != null && badSubs.Any()) + { + var todo = Todo.MakeNew(); + todo.Event = Event; + todo.ToRedilever = badSubs.ToArray(); + SocialGraphTodos.EnqueueDelivery(todo); + } + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Workers/EventDeliveryBase.cs b/src/Agni.Social/Graph/Server/Workers/EventDeliveryBase.cs new file mode 100644 index 0000000..f88b737 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Workers/EventDeliveryBase.cs @@ -0,0 +1,56 @@ +using System; +using Agni.MDB; +using Agni.Workers; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph.Server.Workers +{ + public abstract class EventDeliveryBase : Todo + { + [Field(backendName: "g_emt")] public GDID G_Emitter { get; set; } + [Field(backendName: "evt_gdd")] public GDID Evt_GDID { get; set; } + [Field(backendName: "evt_utc")] public DateTime Evt_TimestampUTC { get; set; } + [Field(backendName: "evt_typ")] public string Evt_EventType { get; set; } + [Field(backendName: "evt_shr")] public GDID Evt_G_TargetShard { get; set; } + [Field(backendName: "evt_tar")] public GDID Evt_G_Target { get; set; } + [Field(backendName: "evt_cfg")] public string Evt_Config { get; set; } + + + private Event? m_Event; + + public Event Event + { + get + { + if (m_Event == null) + m_Event = new Event(Evt_GDID, + Evt_TimestampUTC, + G_Emitter, + Evt_EventType, + Evt_G_TargetShard, + Evt_G_Target, + Evt_Config); + return m_Event.Value; + } + set + { + G_Emitter = value.G_EmitterNode; + Evt_GDID = value.GDID; + Evt_TimestampUTC = value.TimestampUTC; + Evt_EventType = value.EventType; + Evt_G_TargetShard = value.G_TargetShard; + Evt_G_Target = value.G_Target; + Evt_Config = value.Config; + m_Event = null; + } + } + + public GraphHost GraphHost { get { return GraphOperationContext.Instance.GraphHost; } } + + public CRUDOperations ForNode(GDID gNode) + { + return GraphOperationContext.Instance.DataStore.PartitionedOperationsFor(SocialConsts.MDB_AREA_NODE, gNode); + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Workers/EventRedeliverTodo.cs b/src/Agni.Social/Graph/Server/Workers/EventRedeliverTodo.cs new file mode 100644 index 0000000..84265e8 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Workers/EventRedeliverTodo.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Resources; +using Agni.Coordination; +using Agni.MDB; +using Agni.Social.Graph.Server.Data; +using Agni.Workers; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Environment; +using NFX.Log; +using NFX.Serialization.Arow; + +namespace Agni.Social.Graph.Server.Workers +{ + + [Arow] + [TodoQueue(SocialConsts.TQ_EVT_SUB_DELIVER, "A3410533-A9C4-47CD-A0DB-3980C13379F1")] + public sealed class EventRedeliverTodo : EventDeliveryBase + { + + [Field(backendName: "trd")]public SubscriberRow[] ToRedilever { get; set; } + + protected override void DoPrepareForEnqueuePostValidate(string targetName) + { + base.DoPrepareForEnqueuePostValidate(targetName); + var key = G_Emitter.ToString(); + SysShardingKey = key; + SysParallelKey = key; + } + + protected override ExecuteState Execute(ITodoHost host, DateTime utcBatchNow) + { + try + { + IConfigSectionNode cfg = null; + if (Event.Config.IsNotNullOrWhiteSpace()) cfg = Event.Config.AsLaconicConfig(handling: ConvertErrorHandling.ReturnDefault); + var graphHost = GraphOperationContext.Instance.GraphHost; + var badSubs = graphHost.DeliverEventsChunk(ToRedilever, Event, cfg); + if (badSubs != null && badSubs.Any()) + { + ToRedilever = badSubs.ToArray(); + return ExecuteState.ReexecuteUpdated; + } + return ExecuteState.Complete; + } + catch (Exception error) + { + host.Log(MessageType.Error, this, "Redeliver()", error.ToMessageWithType(), error); + } + return ExecuteState.ReexecuteAfterError; + } + + protected override int RetryAfterErrorInMs(DateTime utcBatchNow) + { + if (SysTries < 2) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(1000, 3000); + if (SysTries < 5) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(20000, 60000); + if (SysTries <= 10) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(80000, 160000); + throw new SocialException("Event redeliver after failure {0} retries".Args(10)); + } + + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Workers/EventRemoveFriendsTodo.cs b/src/Agni.Social/Graph/Server/Workers/EventRemoveFriendsTodo.cs new file mode 100644 index 0000000..d8370c6 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Workers/EventRemoveFriendsTodo.cs @@ -0,0 +1,98 @@ +using System; +using Agni.MDB; +using Agni.Social.Graph.Server.Data; +using Agni.Workers; +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Log; +using NFX.Serialization.Arow; + +namespace Agni.Social.Graph.Server.Workers +{ + [Arow] + [TodoQueue(SocialConsts.TQ_EVT_SUB_REMOVE, "AF60C7EF-350D-4241-8FF7-F486024C3A14")] + public sealed class EventRemoveFriendsTodo : Todo + { + /// + /// Индекс текущего друга + /// + [Field(backendName: "frd_idx")] public int FriendIndex { get; set; } + /// + /// Remove by Node + /// + [Field(backendName: "g_nod")] public GDID G_Node { get; set; } + /// + /// Current friend + /// + [Field(backendName: "g_frd")] public GDID G_Friend { get; set; } + + protected override ExecuteState Execute(ITodoHost host, DateTime utcBatchNow) + { + try + { + if (G_Friend.IsZero) G_Friend = getFriend(); + removeFriends(); + G_Friend = getNextFriend(); + if (G_Friend.IsZero) + { + removeFriendList(); + removeFriend(); + return ExecuteState.Complete; + } + return ExecuteState.ReexecuteUpdated; + } + catch (Exception error) + { + host.Log(MessageType.Error, this, "RemoveNode()", error.ToMessageWithType(), error); + } + return ExecuteState.ReexecuteAfterError; + } + + public CRUDOperations ForNode(GDID gNode) + { + return GraphOperationContext.Instance.DataStore.PartitionedOperationsFor(SocialConsts.MDB_AREA_NODE, gNode); + } + + protected override int RetryAfterErrorInMs(DateTime utcBatchNow) + { + if (SysTries < 2) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(1000, 3000); + if (SysTries < 5) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(20000, 60000); + if (SysTries <= 10) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(80000, 160000); + return ExternalRandomGenerator.Instance.NextScaledRandomInteger(1600000, 320000); + // throw new SocialException("Event redeliver after failure {0} retries".Args(10)); + } + + private GDID getNextFriend() + { + FriendIndex = FriendIndex + 1; + return getFriend(); + } + + private GDID getFriend() + { + var qry = Queries.GetNextFriend(G_Node, FriendIndex); + var row = ForNode(G_Node).LoadRow(qry); + return row != null ? row.G_Friend : GDID.Zero; + } + + private void removeFriends() + { + var qry = Queries.DeleteFriendByNode(G_Node); + ForNode(G_Friend).ExecuteWithoutFetch(qry); + } + + private void removeFriendList() + { + var qry = Queries.RemoveFriendListByNode(G_Node); + ForNode(G_Node).ExecuteWithoutFetch(qry); + } + + private void removeFriend() + { + var qry = Queries.RemoveFriendByNode(G_Node); + ForNode(G_Node).ExecuteWithoutFetch(qry); + } + + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Workers/EventRemoveNodeTodo.cs b/src/Agni.Social/Graph/Server/Workers/EventRemoveNodeTodo.cs new file mode 100644 index 0000000..430adcb --- /dev/null +++ b/src/Agni.Social/Graph/Server/Workers/EventRemoveNodeTodo.cs @@ -0,0 +1,100 @@ +using System; +using Agni.MDB; +using Agni.Social.Graph.Server.Data; +using Agni.Workers; +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Log; +using NFX.Serialization.Arow; + +namespace Agni.Social.Graph.Server.Workers +{ + + [Arow] + [TodoQueue(SocialConsts.TQ_EVT_SUB_REMOVE, "A0BE51EB-E20B-4AC3-8615-86E6EDDB62BA")] + public sealed class EventRemoveNodeTodo : Todo + { + /// + /// Увеличивается на OffsetSize по завершении обработки конкретного volume. + /// При селдующей итерации индекс последующего volume вычисляется по формуле : VolumeIteration + OffsetSize + /// + [Field(backendName: "vol_idx")] public int VolumeIndex { get; set; } + /// + /// Remove by Node + /// + [Field(backendName: "g_nod")] public GDID G_Node { get; set; } + /// + /// Current volume subscribers + /// + [Field(backendName: "g_vol")] public GDID G_Volume { get; set; } + + + protected override ExecuteState Execute(ITodoHost host, DateTime utcBatchNow) + { + try + { + if (G_Volume.IsZero) G_Volume = getVolume(); + removeSubscribers(); + G_Volume = getNextVolume(); + if (G_Volume.IsZero) + { + removeVolumes(); + removeNode(); + return ExecuteState.Complete; + } + return ExecuteState.ReexecuteUpdated; + } + catch (Exception error) + { + host.Log(MessageType.Error, this, "RemoveNode()", error.ToMessageWithType(), error); + } + return ExecuteState.ReexecuteAfterError; + } + + public CRUDOperations ForNode(GDID gNode) + { + return GraphOperationContext.Instance.DataStore.PartitionedOperationsFor(SocialConsts.MDB_AREA_NODE, gNode); + } + + protected override int RetryAfterErrorInMs(DateTime utcBatchNow) + { + if (SysTries < 2) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(1000, 3000); + if (SysTries < 5) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(20000, 60000); + if (SysTries <= 10) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(80000, 160000); + return ExternalRandomGenerator.Instance.NextScaledRandomInteger(1600000, 320000); + // throw new SocialException("Event redeliver after failure {0} retries".Args(10)); + } + + private GDID getNextVolume() + { + VolumeIndex = VolumeIndex + 1; + return getVolume(); + } + + private GDID getVolume() + { + var qry = Queries.GetNextVolume(G_Node, VolumeIndex); + var row = ForNode(G_Node).LoadRow(qry); + return row != null ? row.G_SubscriberVolume : GDID.Zero; + } + + private void removeSubscribers() + { + var qry = Queries.RemoveSubscribers(G_Volume); + ForNode(G_Node).ExecuteWithoutFetch(qry); + } + + private void removeVolumes() + { + var qry = Queries.RemoveSubVol(G_Node); + ForNode(G_Node).ExecuteWithoutFetch(qry); + } + + private void removeNode() + { + var qry = Queries.RemoveNode(G_Node); + ForNode(G_Node).ExecuteWithoutFetch(qry); + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Workers/EventSubscribeTodo.cs b/src/Agni.Social/Graph/Server/Workers/EventSubscribeTodo.cs new file mode 100644 index 0000000..c4ceafd --- /dev/null +++ b/src/Agni.Social/Graph/Server/Workers/EventSubscribeTodo.cs @@ -0,0 +1,99 @@ +using System; +using Agni.Social.Graph.Server.Data; +using Agni.Workers; +using NFX; +using NFX.DataAccess; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Log; +using NFX.Serialization.Arow; + +namespace Agni.Social.Graph.Server.Workers +{ + + [Arow] + [TodoQueue(SocialConsts.TQ_EVT_SUB_MANAGE, "A00C4F64-DBE0-4B86-8132-6FD77B684243")] + public sealed class EventSubscribeTodo : EventSubscriptionBase + { + [Field(backendName: "stp")] public string Subs_Type { get; set; } + [Field(backendName: "par")] public byte[] Parameters { get; set; } + + protected override void DoPrepareForEnqueuePostValidate(string targetName) + { + base.DoPrepareForEnqueuePostValidate(targetName); + var key = G_Owner.ToString(); + SysShardingKey = key; + SysParallelKey = key; + } + + protected override ExecuteState Execute(ITodoHost host, DateTime utcBatchNow) + { + try + { + SubscriberVolumeRow minCntVol; + var existing = FindSubscriber(out minCntVol); + if (existing != null) return ExecuteState.Complete; + if (minCntVol == null) minCntVol = createVolume(); + + var row = new SubscriberRow() + { + G_SubscriberVolume = minCntVol.G_SubscriberVolume, + G_Subscriber = G_Subscriber, + Subs_Type = Subs_Type, + Create_Date = App.TimeSource.UTCNow, + Parameters = Parameters + }; + + try + { + ForNode(row.G_SubscriberVolume).Insert(row); + } + catch (DataAccessException dae) + { + if(dae.KeyViolation.IsNotNullOrWhiteSpace() ) return ExecuteState.Complete; + throw; + } + + try + { + minCntVol.Count++; + ForNode(G_Owner).Update(minCntVol, filter: "Count".OnlyTheseFields() ); + } + catch (Exception error) + { + host.Log(MessageType.Error, this, "Subscribe()", error.ToMessageWithType(), error); + } + + + return ExecuteState.Complete; + } + catch (Exception error) + { + host.Log(MessageType.Error, this, "Subscribe()", error.ToMessageWithType(), error); + return ExecuteState.ReexecuteAfterError; + } + } + + protected override int RetryAfterErrorInMs(DateTime utcBatchNow) + { + if (SysTries < 2) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(1000, 3000); + if (SysTries < 5) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(20000, 60000); + if (SysTries <= 10) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(80000, 160000); + throw new GraphException("Event subscription after failure {0} retries".Args(10)); + } + + private SubscriberVolumeRow createVolume() + { + var gVol = NodeRow.GenerateNewNodeRowGDID(); // NOTICE: Volume uses Node Row GDID (Briefcase key) + var result = new SubscriberVolumeRow() + { + G_Owner = G_Owner, + G_SubscriberVolume = gVol, + Create_Date = App.TimeSource.UTCNow, + Count = 0 + }; + ForNode(G_Owner).Insert(result); + return result; + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Workers/EventSubscriptionBase.cs b/src/Agni.Social/Graph/Server/Workers/EventSubscriptionBase.cs new file mode 100644 index 0000000..2c4fa77 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Workers/EventSubscriptionBase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Agni.MDB; +using Agni.Social.Graph.Server.Data; +using Agni.Workers; +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; + +namespace Agni.Social.Graph.Server.Workers +{ + public abstract class EventSubscriptionBase : Todo + { + [Field(backendName: "g_own")] public GDID G_Owner { get; set; } + [Field(backendName: "g_sub")] public GDID G_Subscriber { get; set; } + + protected virtual SubscriberRow FindSubscriber(out SubscriberVolumeRow minCntVol) + { + minCntVol = null; + IEnumerable volumes = GetSubscriberVolumes(); + SubscriberRow result = null; + var i = 0; + foreach (var volume in volumes) + { + var qry = Queries.FindSubscriber(volume, G_Subscriber); + if (result == null) result = ForNode(volume.G_SubscriberVolume).LoadRow(qry); + if ( (volume.Count < SocialConsts.GetVolumeMaxCountForPosition(i++)) && (minCntVol == null || minCntVol.Count > volume.Count) ) minCntVol = volume; + } + return result; + } + + protected virtual IEnumerable GetSubscriberVolumes() + { + var qry = Queries.FindSubscriberVolumes(G_Owner); + return ForNode(G_Owner).LoadEnumerable(qry); + } + + public CRUDOperations ForNode(GDID gNode) + { + return GraphOperationContext.Instance.DataStore.PartitionedOperationsFor(SocialConsts.MDB_AREA_NODE, gNode); + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Graph/Server/Workers/EventUnsubscribeTodo.cs b/src/Agni.Social/Graph/Server/Workers/EventUnsubscribeTodo.cs new file mode 100644 index 0000000..8680725 --- /dev/null +++ b/src/Agni.Social/Graph/Server/Workers/EventUnsubscribeTodo.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Resources; +using Agni.Coordination; +using Agni.Social.Graph.Server.Data; +using Agni.Workers; + +using NFX; +using NFX.DataAccess.CRUD; +using NFX.DataAccess.Distributed; +using NFX.Log; +using NFX.Serialization.Arow; + +namespace Agni.Social.Graph.Server.Workers +{ + + [Arow] + [TodoQueue(SocialConsts.TQ_EVT_SUB_MANAGE, "A166739E-6034-4390-B961-A9B51C858B80")] + public sealed class EventUnsubscribeTodo : EventSubscriptionBase + { + protected override void DoPrepareForEnqueuePostValidate(string targetName) + { + base.DoPrepareForEnqueuePostValidate(targetName); + var key = G_Owner.ToString(); + SysShardingKey = key; + SysParallelKey = key; + } + + protected override ExecuteState Execute(ITodoHost host, DateTime utcBatchNow) + { + try + { + SubscriberVolumeRow minColVol; + var row = FindSubscriber(out minColVol); + if (row == null ) return ExecuteState.ReexecuteAfterError; + ForNode(row.G_SubscriberVolume).Delete(row); + return ExecuteState.Complete; + } + catch (Exception error) + { + host.Log(MessageType.Error, this, "Unsubscribe()", error.ToMessageWithType(), error); + return ExecuteState.ReexecuteAfterError; + } + } + + protected override int RetryAfterErrorInMs(DateTime utcBatchNow) + { + if (SysTries < 2) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(1000, 3000); + if (SysTries < 5) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(20000, 60000); + if (SysTries <= 10) return ExternalRandomGenerator.Instance.NextScaledRandomInteger(80000, 160000); + throw new GraphException("Event subscription after failure {0} retries".Args(10)); + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Properties/AssemblyInfo.cs b/src/Agni.Social/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9205e56 --- /dev/null +++ b/src/Agni.Social/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; diff --git a/src/Agni.Social/SocialConsts.cs b/src/Agni.Social/SocialConsts.cs new file mode 100644 index 0000000..de3a0d9 --- /dev/null +++ b/src/Agni.Social/SocialConsts.cs @@ -0,0 +1,55 @@ +using System; + +namespace Agni.Social +{ + public static class SocialConsts + { + public const string TQ_EVT_SUB_MANAGE = "esub-manage"; + public const string TQ_EVT_SUB_DELIVER = "esub-deliver"; + public const string TQ_EVT_SUB_REMOVE = "esub-remove"; + + public const string CONFIG_GRAPH_HOST_SECTION = "graph-host"; + public const string CONFIG_DATA_STORE_SECTION = "data-store"; + public const string CONFIG_GRAPH_COMMENT_FETCH_STRATEGY_SECTION = "graph-comment-fetch-strategy"; + public const string CONFIG_GRAPH_NODE_HOST_SET_ATTR = "node-hostset"; + public const string CONFIG_GRAPH_COMMENT_HOST_SET_ATTR = "comment-hostset"; + public const string CONFIG_GRAPH_FRIEND_HOST_SET_ATTR = "friend-hostset"; + public const string CONFIG_GRAPH_EVENT_HOST_SET_ATTR = "event-hostset"; + + public const string CONFIG_TRENDING_HOST_SET_ATTR = "trending-hostset"; + + //public const string GS_DS_TARGET = "MDB.GRAPH"; + //public const string GS_DS_MYSQL_TARGET = "MDB.GRAPH.MYSQL"; + + public const string MDB_AREA_NODE = "node"; + public const string MDB_AREA_COMMENT = "comment"; + + public const string GS_FRIEND_LIST_SEPARATOR = ","; + + public const string GS_NODE_TBL = "GraphSystemService.Node"; + public const string GS_COMMENT_BLOCK_TBL = "GraphSystemService.CommentBlock"; + + public const int GS_MAX_FRIENDS_COUNT = 10000; + public const int GS_MAX_SUBSCRIBERS_COUNT = 10000; + + public const string HOST_SET_SOCIAL_GRAPH = "socialgraphtodo"; + public const string SVC_SOCIAL_GRAPH_TODO = "socialgraphtodoqueue"; + public const string HOST_SET_SUBS_DELIVERY = "subsdeliverytodo"; + public const string SVC_SUBS_DELIBERY_TODO = SVC_SOCIAL_GRAPH_TODO; //"subsdeliverytodoqueue"; + public const string HOST_SET_SUBS_REMOVE = "removetodo"; + public const string SVC_SUBS_REMOVE_TODO = SVC_SOCIAL_GRAPH_TODO; //"removetodoqueue"; + + public const int SUBSCRIPTION_DELIVERY_CHUNK_SIZE = 0xff; + public const int SUBSCRIPTION_DELIVERY_MAX_CHUNK_SIZE = SUBSCRIPTION_DELIVERY_CHUNK_SIZE * 8; + + public static int GetVolumeMaxCountForPosition(int i) + { + if (i < 5) return SUBSCRIPTION_DELIVERY_CHUNK_SIZE - 1; + if (i < 10) return SUBSCRIPTION_DELIVERY_CHUNK_SIZE * 2; + if (i < 15) return SUBSCRIPTION_DELIVERY_CHUNK_SIZE * 4 ; + return SUBSCRIPTION_DELIVERY_MAX_CHUNK_SIZE; + } + + public const string GS_GENERAL_RATING_DIMENSION = "general"; + } +} diff --git a/src/Agni.Social/SocialGraphTodos.cs b/src/Agni.Social/SocialGraphTodos.cs new file mode 100644 index 0000000..ff933dd --- /dev/null +++ b/src/Agni.Social/SocialGraphTodos.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Agni.Workers; + +namespace Agni.Social +{ + public static class SocialGraphTodos + { + public static void EnqueueSubscribtion(Todo todo) { AgniSystem.ProcessManager.Enqueue(todo, SocialConsts.HOST_SET_SOCIAL_GRAPH, SocialConsts.SVC_SOCIAL_GRAPH_TODO); } + public static void EnqueueSubscribtion(IEnumerable todos) { AgniSystem.ProcessManager.Enqueue(todos, SocialConsts.HOST_SET_SOCIAL_GRAPH, SocialConsts.SVC_SOCIAL_GRAPH_TODO); } + public static Task Async_EnqueueSubscribtion(Todo todo) { return AgniSystem.ProcessManager.Async_Enqueue(todo, SocialConsts.HOST_SET_SOCIAL_GRAPH, SocialConsts.SVC_SOCIAL_GRAPH_TODO); } + public static Task Async_EnqueueSubscribtion(IEnumerable todos) { return AgniSystem.ProcessManager.Async_Enqueue(todos, SocialConsts.HOST_SET_SOCIAL_GRAPH, SocialConsts.SVC_SOCIAL_GRAPH_TODO); } + + public static void EnqueueDelivery(Todo todo) { AgniSystem.ProcessManager.Enqueue(todo, SocialConsts.HOST_SET_SUBS_DELIVERY, SocialConsts.SVC_SUBS_DELIBERY_TODO); } + public static void EnqueueDelivery(IEnumerable todos) { AgniSystem.ProcessManager.Enqueue(todos, SocialConsts.HOST_SET_SUBS_DELIVERY, SocialConsts.SVC_SUBS_DELIBERY_TODO); } + public static Task Async_EnqueueDelivery(Todo todo) { return AgniSystem.ProcessManager.Async_Enqueue(todo, SocialConsts.HOST_SET_SUBS_DELIVERY, SocialConsts.SVC_SUBS_DELIBERY_TODO); } + public static Task Async_EnqueueDelivery(IEnumerable todos) { return AgniSystem.ProcessManager.Async_Enqueue(todos, SocialConsts.HOST_SET_SUBS_DELIVERY, SocialConsts.SVC_SUBS_DELIBERY_TODO); } + + public static void EnqueueRemove(Todo todo) { AgniSystem.ProcessManager.Enqueue(todo, SocialConsts.HOST_SET_SUBS_REMOVE, SocialConsts.SVC_SUBS_REMOVE_TODO); } + public static void EnqueueRemove(IEnumerable todos) { AgniSystem.ProcessManager.Enqueue(todos, SocialConsts.HOST_SET_SUBS_REMOVE, SocialConsts.SVC_SUBS_REMOVE_TODO); } + public static Task Async_EnqueueRemove(Todo todo) { return AgniSystem.ProcessManager.Async_Enqueue(todo, SocialConsts.HOST_SET_SUBS_REMOVE, SocialConsts.SVC_SUBS_REMOVE_TODO); } + public static Task Async_EnqueueRemove(IEnumerable todos) { return AgniSystem.ProcessManager.Async_Enqueue(todos, SocialConsts.HOST_SET_SUBS_REMOVE, SocialConsts.SVC_SUBS_REMOVE_TODO); } + } +} \ No newline at end of file diff --git a/src/Agni.Social/StringConsts.cs b/src/Agni.Social/StringConsts.cs new file mode 100644 index 0000000..fa9e0e5 --- /dev/null +++ b/src/Agni.Social/StringConsts.cs @@ -0,0 +1,74 @@ +using System; + +namespace Agni.Social +{ + internal static class StringConsts + { + + public const string ARGUMENT_ERROR = "Argument error: "; + + public const string TS_SERVICE_NO_VOLUMES_ERROR = "{0} service start error - no volumes configured"; + public const string TS_SERVICE_NO_TRENDING_HOST_ERROR = "{0} service start error - no trending host configured"; + public const string TS_SERVICE_DUPLICATE_VOLUMES_ERROR = "{0} service config error - duplicate volume name: '{1}'"; + public const string TS_INSTANCE_NOT_ALLOCATED_ERROR = "{0} is not allocated"; + public const string TS_INSTANCE_ALREADY_ALLOCATED_ERROR = "{0} is already allocated"; + public const string TS_INSTANCE_DATA_LAYER_IS_NOT_ALLOCATED_ERROR = "{0} Instance data Layer is not allocated"; + public const string TS_HOST_SET_NOT_CONFIG_ERROR = "Error config {0}"; + public const string TS_VOLUME_UNKNOWN_DETALIZATION_ERROR = "Unknown volume detalization level: '{0}'"; + + public const string GS_INSTANCE_NOT_ALLOCATED_ERROR = "{0} is not allocated"; + public const string GS_INSTANCE_ALREADY_ALLOCATED_ERROR = "{0} is already allocated"; + public const string GS_SERVICE_NO_GRAPH_HOST_ERROR = "{0} service start error - no graph host configured"; + public const string GS_SAVE_NODE_ERROR = "Error save graph node {0}"; + public const string GS_GET_NODE_ERROR = "Error get graph node {0}"; + public const string GS_DELETE_NODE_ERROR = "Error delete graph node {0}"; + public const string GS_EMIT_EVENT_ERROR = "Error emit event {0}"; + public const string GS_SUBSCRIBE_ERROR = "Error subscribe {0} to the {1}"; + public const string GS_UNSUBSCRIBE_ERROR = "{0} unsubscription error on {1}"; + public const string GS_ESTIMATE_SUBSCRIPTION_COUNT_ERROR = "Estimate subscription count error {0}"; + public const string GS_GET_FRIEND_LISTS_ERROR = "Error get friend lists {0}"; + public const string GS_ADD_FRIEND_LIST_ERROR = "Error add friend list {0}"; + public const string GS_DELETE_FRIEND_LIST_ERROR = "Error delete friend list {0}"; + public const string GS_GET_FRIEND_CONNECTIONS_ERROR = "Error get friend connections {0}"; + public const string GS_ADD_FRIEND_ERROR = "Error add friend {1} to {0}"; + public const string GS_ASSIGN_FRIEND_LISTS_ERROR = "Error assign friend lists from {1} to {0}"; + public const string GS_DELETE_FRIEND_ERROR = "Error delete friend {1} from {1}"; + public const string GS_MAX_FRIENDS_IN_NODE = "The node {0} exceeds the maximum allowed number of friends"; + public const string GS_INSTANCE_DATA_LAYER_IS_NOT_ALLOCATED_ERROR = "{0} Instance data Layer is not allocated"; + public const string GS_FRIEND_DRIECTION_ERROR = "Friendship not supported from {0} to {1}"; + public const string GS_CAN_NOT_BE_SUSBCRIBED_ERROR = "{0} can not be susbscribed {1}"; + public const string GS_GET_SUBSCRIBER_ERROR = "Error get subscriber for node {0}"; + public const string GS_CAN_NOT_CREATE_COMMENT_ERROR = "Author '{0}' can not create comment for target '{1}' with rating '{2}'"; + public const string GS_CAN_NOT_CREATE_RESPONSE_ERROR = "Author '{0}' can not create response for target '{1}'"; + + public const string GS_INIT_NOT_CONF_ERRROR = "{0} init error: {1} not configured"; + + public const string GS_GET_RATING_ERROR = "Error read rating for node {0}"; + public const string GS_DELETE_RATING_ERROR = "Error delete rating {0}"; + public const string GS_CREATE_RATING_ERROR = "Error create rating in comment {0}."; + public const string GS_UPDATE_RATING_ERROR = "Error update rating {0}."; + public const string GS_GET_RATING_DETAILS_ERROR = "Error get details rating for node {0}, dimensional {1}"; + public const string GS_SET_LIKE_ERROR = "Error change like and dislike for {0} rating message"; + public const string GS_CREATE_RATING_ACCESS_DENIED_ERROR = "Access denied create rating [{1}, {2}] for [{0}]"; + public const string GS_CREATE_UPDATE_RATING_ERROR = "Error create response for the message [{0}] from user [1]"; + public const string GS_RATING_NOT_FOUND = "Rating {0} not found"; + public const string GS_CAN_NOT_BE_RATE_ERROR = " {0} can not be rate {1}"; + public const string GS_MAX_MESSAGE_IN_RATING = "The rating {0} exceeds the maximum allowed number of message"; + public const string GS_FETCH_RATING_ERROR = "Error fetching rating for node {0}"; + public const string GS_CREATE_COMMENT_ERROR = "Error add comment for node '{0}' by author {1}"; + public const string GS_RESPONSE_BAD_PARENT_ID_ERROR = "Bad Parent CommentID (CommentID.IsZero == true)"; + public const string GS_RESPONSE_VOLUME_MISSING_ERROR = "Missing volume for create response comment"; + public const string GS_RESPONSE_COMMENT_ERROR = "Error create response for comment '{0}'"; + public const string GS_COMMENT_NOT_FOUND = "Comment '{0}' not found"; + public const string GS_PARENT_ID_NOT_ROOT = "Parent comment '{0}' is not root"; + public const string GS_NODE_RATING_NOT_FOUND = "Summary rating not found for node '{0}'"; + public const string GS_DELETE_COMMENT_ERROR = "Error delete comment '{0}'"; + public const string GS_FETCH_RESPONSE_ERROR = "Error fetching response for comment '{0}'"; + public const string GS_FETCH_COMPLAINTS_ERROR = "Error fetching complaints for comment '{0}'"; + public const string GS_GET_COMMENT_ERROR = "Comment not found '{0}'"; + public const string GS_COMPLAINT_ERROR = "Error while creating a complaint about comment '{0}'"; + public const string GS_JUSTIFY_COMMENT_ERROR = "Error while justifying comment '{0}'"; + + public const string GS_HOST_SET_NOT_FOUND = "Graph system hostset '{0}' not found"; + } +} diff --git a/src/Agni.Social/Trending/ITrendingSystem.cs b/src/Agni.Social/Trending/ITrendingSystem.cs new file mode 100644 index 0000000..b30aed2 --- /dev/null +++ b/src/Agni.Social/Trending/ITrendingSystem.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using NFX; +using NFX.Glue; +using NFX.DataAccess.Distributed; + +using Agni.Contracts; + +namespace Agni.Social.Trending +{ + /// + /// Represents a facade for trending system that records and keeps track of trending items. + /// Use TrandingSystemInstrumentationProvider to upload the SocialTrendingGauge instances + /// + [Glued] + [LifeCycle(ServerInstanceMode.Singleton)] + public interface ITrendingSystem : IAgniService + { + /// + /// Uploads the batch of SocialTrendingGauge samples to the server + /// + [OneWay] + void Send(SocialTrendingGauge[] gauges); + + /// + /// Returns the specified number of entities entities in a data range starting from position for the specified entity type + /// Pass null tEntity to select the top entities across various types + /// + IEnumerable GetTrending(TrendingQuery query); + } + + /// + /// Represents query parameters sent to ITrendingSystem.GetTrending(query) + /// + public struct TrendingQuery + { + public const int MAX_FETCH_COUNT = 1024; + public const int MAX_SAMPLE_COUNT = 1024; + + /// If null, then trending across all types is queried + /// The start timespan of sampling + /// The end timespan of sampling + /// How many samples we want to get in a date range + /// The starting ranking position of trending + /// + /// The count of trending records + public TrendingQuery(string tEntity, DateTime startDate, DateTime endDate, int sampleCount, int fetchStart, int fetchCount, string filter) + { + if (!SocialTrendingGauge.TryValidateEntityName(tEntity)) + throw new SocialException(StringConsts.ARGUMENT_ERROR + "TrendingQuery.ctor(tEntity!Valid:'{0}')".Args(tEntity)); + + EntityType = tEntity; + StartDate = startDate; + EndDate = endDate; + SampleCount = sampleCount < 0 ? 1 : sampleCount > MAX_SAMPLE_COUNT ? MAX_SAMPLE_COUNT : sampleCount; + FetchStart = fetchStart < 0 ? 0 : fetchStart; + FetchCount = fetchCount < 0 ? 1 : fetchCount > MAX_FETCH_COUNT ? MAX_FETCH_COUNT : fetchCount; + DimensionFilter = filter; + } + + /// + /// If null, then trending across all types is queried + /// + public readonly string EntityType; + + /// + /// The start timespan of sampling + /// + public readonly DateTime StartDate; + + /// + /// The end timespan of sampling + /// + public readonly DateTime EndDate; + + /// + /// How many samples we want to get in a date range + /// + public readonly int SampleCount; + + /// + /// The starting ranking position of trending + /// + public readonly int FetchStart; + + /// + /// The count of trending records + /// + public readonly int FetchCount; + + + /// + /// The dimension filter in laconic format + /// + public readonly string DimensionFilter; + + } + + + /// + /// Contains information about the trending entities + /// + [Serializable] + public struct TrendingEntity + { + public TrendingEntity(DateTime dt, int durationMin, string etp, GDID gShard, GDID gEntity, ulong count) + { + TimestampUTC = dt; + DurationMinutes = durationMin; + EntityType = etp; + G_Shard = gShard; + G_Entity = gEntity; + Count = count; + } + + /// The type of entity, such as "user", "group", "forum" + public readonly DateTime TimestampUTC; + + /// The duration of time span in minutes + public readonly int DurationMinutes; + + /// The type of entity, such as "user", "group", "forum" + public readonly string EntityType; + + /// The entity sharding key + public readonly GDID G_Shard; + + /// The entity identity + public readonly GDID G_Entity; + + /// The Count of events for the entity + public readonly ulong Count; + } + + + /// + /// Contract for client of ITrendingSystem svc + /// + public interface ITrendingSystemClient : IAgniServiceClient, ITrendingSystem + { + CallSlot Async_GetTrending(TrendingQuery query); + } +} diff --git a/src/Agni.Social/Trending/Server/TODO.txt b/src/Agni.Social/Trending/Server/TODO.txt new file mode 100644 index 0000000..0b8daf8 --- /dev/null +++ b/src/Agni.Social/Trending/Server/TODO.txt @@ -0,0 +1,64 @@ +Server/ + will contain the server implementation of the ITrendingSystem contract + + you can take the pattern from /Agni/WebMessaging/Server: + + + +TrendingSystemServer singleton glue trampoline that calls + +TrendingSystemService : Service (see how all of this is done in WebMessaging) + + TrendingSystemService is sealed, and coupled with ITrendingVolumeStore, the implementation of which is db-dependent. + Use MongoDB + Mongo-related implementation has to reside in /Agni/Providers/Agni.Social.MongoDB.dll (do not forget to set proper relative output directories) + + ATTENTION! The ITrendingSystem will work in a hostset containing more than 1 server. + It is important to keep this in mind while coordinating the data purging between the volumes + so multiple servers do not do the same job twice. + + +=================================================================== + + Trending +Trending - is a few views/reports shown on a front page similar to Twitter. +Trending will be very easy to implement using Agni telemetry. +Trending will be implemented as a stand-alone application with its own datastore and interface the cluster components via Glue. + +The trending engine/service is fed from all of the subscription events. Basically it is a telemetry store that records the COUNT: long per key. +The keys are the G_EMITTER and Dimensions supplied as laconic vector. + +We harvest the telemetry in a hierarchical cluster akin to regular telemetry - saving the “trending” channel into a special data store using SocialTrendingGauge. +The data store is split in -sub-data stores each connected to a "volume" ("tom" po-russki). + +class Volume: INamed, IOrdered. + +Volumes have level of detail expressed as a date mask, and maximum age of data kept. +Every trending sample gets written into every volume, however the volume-dependent reduction (level of details) is performed by the date key +"YYYYMMDD HHF" - maximum +"YYYYMM" - minimum +F = 1/12 fraction of the hours (5 minute interval) +The date field is keps as date in MongoDB, the reduced date parts replaced by constants, for example: + 03/18/2018 2:34 am -> 03/01/2018 1:00pm reduced to YYYYMM + 03/18/2018 2:34 am -> 03/18/2018 1:00pm reduced to YYYYMMDD + 03/18/2018 2:34 am -> 03/18/2018 2:00am reduced to YYYYMMDD HH + 03/18/2018 2:34 am -> 03/18/2018 2:30am reduced to YYYYMMDD HH F + + +The table structure is modeled after the SocialTrendingGauge + +table name: "entity" + _id + g_entity: gdid + g_shard: gdid + date: date <---- the resolution of the date depends on volume, e.g all monthly reads will be YYYY/MM/01 1:00 PM etc... + count: ulong + + dim1 + dim2 + .... + dimX + +The mapping of SoicalTrendingMessage.Dimensions -> "dim_*" columns is performed via IDimensionMapper - implementing class +(implemented in the PARTICULAR business system) + +The service periodically scans all volumes and deletes old data from the database. + diff --git a/src/Agni.Social/Trending/Server/TrendingSystemHost.cs b/src/Agni.Social/Trending/Server/TrendingSystemHost.cs new file mode 100644 index 0000000..fb1b71d --- /dev/null +++ b/src/Agni.Social/Trending/Server/TrendingSystemHost.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +using NFX.ApplicationModel; +using NFX.Environment; + +namespace Agni.Social.Trending.Server +{ + /// + /// Implemented in the particular business system to map SocialTrendingGauge.Dimensions vector into the KVP that can be stored in the database + /// + public abstract class TrendingSystemHost : ApplicationComponent + { + protected TrendingSystemHost(TrendingSystemService director, IConfigSectionNode config) : base(director) + { + ConfigAttribute.Apply(this, config); + } + + /// + /// Return all entity names supported by the system + /// + public abstract IEnumerable AllEntities { get; } + + /// + /// Returns true if entity is supported by the particular system + /// + public abstract bool HasEntity(string tEntity); + + /// + /// Returns ordered set of dimensions for the entity + /// + public abstract string[] GetDimensionNamesForEntity(string tEntity); + + /// + /// Maps gauge packed string dimensions into dictionary (i.e. using JSON or Laconic) + /// + public abstract IEnumerable> MapGaugeDimensions(string tEntity, string dimensions); + + /// + /// Maps dictionary dimensions into packed string (i.e. using JSON or Laconic) + /// + public abstract string MapGaugeDimensions(string tEntity, IEnumerable> dimensions); + } +} diff --git a/src/Agni.Social/Trending/Server/TrendingSystemServer.cs b/src/Agni.Social/Trending/Server/TrendingSystemServer.cs new file mode 100644 index 0000000..d5df1c0 --- /dev/null +++ b/src/Agni.Social/Trending/Server/TrendingSystemServer.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Agni.Social.Trending.Server +{ + /// + /// Implemnts Glue server per ITrendingSystem contract + /// + public sealed class TrendingSystemServer : ITrendingSystem + { + public void Send(SocialTrendingGauge[] gauges) + { + TrendingSystemService.Instance.Send(gauges); + } + + public IEnumerable GetTrending(TrendingQuery query) + { + return TrendingSystemService.Instance.GetTrending(query); + } + } +} \ No newline at end of file diff --git a/src/Agni.Social/Trending/Server/TrendingSystemService.cs b/src/Agni.Social/Trending/Server/TrendingSystemService.cs new file mode 100644 index 0000000..982e568 --- /dev/null +++ b/src/Agni.Social/Trending/Server/TrendingSystemService.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using NFX; +using NFX.Environment; +using NFX.ServiceModel; +using NFX.Time; + +using Agni.WebMessaging; + +namespace Agni.Social.Trending.Server +{ + /// + /// Implemnts ITrendingSystem contract + /// + public sealed class TrendingSystemService : ServiceWithInstrumentationBase, ITrendingSystem + { + #region CONSTS + public const string CONFIG_VOLUME_SECTION = "volume"; + public const string CONFIG_TRENDING_HOST_SECTION = "trending-host"; + #endregion + + #region STATIC/.ctor + private static object s_Lock = new object(); + private static volatile TrendingSystemService s_Instance; + + internal static TrendingSystemService Instance + { + get + { + var instance = s_Instance; + if (instance == null) + throw new WebMessagingException(StringConsts.TS_INSTANCE_NOT_ALLOCATED_ERROR.Args(typeof(TrendingSystemService).Name)); + return instance; + } + } + /// + /// Выбор детализации + /// + public static VolumeDetalizationLevel CalcDetalizationLevelForRange(DateTime startDate, DateTime endDate, int sampleCount) + { + var d1 = endDate < startDate ? endDate : startDate; + var d2 = endDate >= startDate ? endDate : startDate; + var span = d2 - d1; + + if (span.TotalMinutes < 5 * sampleCount) return VolumeDetalizationLevel.Fractional; + if (span.TotalHours < sampleCount) return VolumeDetalizationLevel.Hourly; + if (span.TotalDays < sampleCount) return VolumeDetalizationLevel.Daily; + if (span.TotalDays < 7 * sampleCount) return VolumeDetalizationLevel.Weekly; + if (span.TotalDays < 30 * sampleCount) return VolumeDetalizationLevel.Monthly; + + return VolumeDetalizationLevel.Quarter; + } + + public TrendingSystemService(object director) : base(director) + { + lock (s_Lock) + { + if (s_Instance != null) + throw new WebMessagingException(StringConsts.TS_INSTANCE_ALREADY_ALLOCATED_ERROR.Args(GetType().Name)); + + m_Volumes = new Registry(); + + s_Instance = this; + } + } + + protected override void Destructor() + { + lock (s_Lock) + { + base.Destructor(); + deleteVolumes(); + s_Instance = null; + } + } + + #endregion + + #region Fields + + private Registry m_Volumes; + private Event m_ManagerEvent; + private TrendingSystemHost m_TrendingHost; + + #endregion; + + #region Properties + public override bool InstrumentationEnabled { get; set; } + + public TrendingSystemHost TrendingHost { get { return m_TrendingHost; } } + + #endregion + + #region Public + + public void Send(SocialTrendingGauge[] gauges) + { + if (gauges == null) return; + foreach (var gauge in gauges) + { + if (!TrendingHost.HasEntity(gauge.Entity)) + { + // TODO Log error "unsupported entity" with throttling (1 error per 1 minute) + continue; + } + + foreach (var volume in m_Volumes) + { + volume.WriteGauge(gauge); + } + } + } + + public IEnumerable GetTrending(TrendingQuery query) + { + var volume = findVolume(query.StartDate, query.EndDate, query.SampleCount); + return volume != null ? volume.GetTreding(query) : Enumerable.Empty(); + } + + #endregion + + #region Protected + + protected override void DoConfigure(IConfigSectionNode node) + { + base.DoConfigure(node); + deleteVolumes(); + if (node == null || !node.Exists) return; + + var nHost = node[CONFIG_TRENDING_HOST_SECTION]; + m_TrendingHost = FactoryUtils.Make(nHost, args: new object[]{ this, nHost }); + + foreach (var cnode in node.Children.Where(cn => cn.IsSameName(CONFIG_VOLUME_SECTION))) + { + var volume = FactoryUtils.MakeAndConfigure(cnode, args: new object[] { this }); + if (!m_Volumes.Register(volume)) + throw new WebMessagingException(StringConsts.TS_SERVICE_DUPLICATE_VOLUMES_ERROR.Args(GetType().Name, volume.Name)); + } + } + + protected override void DoStart() + { + if (m_Volumes.Count==0) + throw new WebMessagingException(StringConsts.TS_SERVICE_NO_VOLUMES_ERROR.Args(GetType().Name)); + + if (m_TrendingHost == null) + throw new WebMessagingException(StringConsts.TS_SERVICE_NO_TRENDING_HOST_ERROR.Args(GetType().Name)); + + m_Volumes.ForEach(v => v.Start()); + + m_ManagerEvent = new Event(App.EventTimer, + interval: TimeSpan.FromMilliseconds(3759), + body: _ => this.AcceptManagerVisit(this, _.LocalizedTime), + bodyAsyncModel: EventBodyAsyncModel.AsyncTask + ); + base.DoStart(); + } + + protected override void DoSignalStop() + { + base.DoSignalStop(); + DisposeAndNull(ref m_ManagerEvent); + m_Volumes.ForEach(v => v.SignalStop()); + } + + protected override void DoWaitForCompleteStop() + { + base.DoWaitForCompleteStop(); + + m_Volumes.ForEach(v => v.WaitForCompleteStop()); + } + + protected override void DoAcceptManagerVisit(object manager, DateTime managerNow) + { + base.DoAcceptManagerVisit(manager, managerNow); + managerNow = App.TimeSource.UTCNow; + m_Volumes.ForEach(v => v.AcceptManagerVisit(this, managerNow)); + } + + #endregion + + #region pvt + + private void deleteVolumes() + { + m_Volumes.ForEach(v => v.Dispose()); + m_Volumes.Clear(); + } + + private Volume findVolume(DateTime startDate, DateTime endDate, int sampleCount) + { + var level = CalcDetalizationLevelForRange(startDate, endDate, sampleCount); + return m_Volumes.FirstOrDefault(v => v.DetalizationLevel == level); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Agni.Social/Trending/Server/Volume.cs b/src/Agni.Social/Trending/Server/Volume.cs new file mode 100644 index 0000000..26c0f09 --- /dev/null +++ b/src/Agni.Social/Trending/Server/Volume.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using NFX; +using NFX.CodeAnalysis; +using NFX.DataAccess.Distributed; +using NFX.Environment; +using NFX.ServiceModel; +using MessageType = NFX.Log.MessageType; + +namespace Agni.Social.Trending.Server +{ + /// + /// Denotes the levels of time granularity - the level of detail + /// + public enum VolumeDetalizationLevel + { + /// + /// YYYY MM + /// + Quarter, + /// + /// YYYY MM + /// + Monthly, + /// + /// YYYY MM + /// + Weekly, + /// + /// YYYY MM DD + /// + Daily, + /// + /// YYYY MM DD HH + /// + Hourly, + /// + /// YYYY MM DD HH F + /// + Fractional + } + + /// + /// Represents an entity that stores trending data per the specified detalization level. + /// This class is NOT thread-safe + /// + public abstract class Volume : ServiceWithInstrumentationBase + { + #region CONSTS + public const int MIN_HISTORY_LENGTH_DAYS = 30; + public const int MIN_OLD_DATA_PURGE_FREQUENCY_MINUTES = 15; + public const int FRACTIONAL_FREQUENCY_MINUTES = 5; + #endregion + + #region Static/.ctor + /// + /// Rounds the specified UTC date by discarding the extra date/time components per supplied detalization level + /// + public static DateTime RoundDatePerDetalization(DateTime utcDate, VolumeDetalizationLevel level) + { + switch (level) + { + case VolumeDetalizationLevel.Quarter: + var mi = utcDate.Month - 1; + mi -= (mi % 3); + var month = mi + 1; + return new DateTime(utcDate.Year, month, day: 1, hour: 12, minute: 0, second: 0, kind: DateTimeKind.Utc ); + + case VolumeDetalizationLevel.Monthly: + return new DateTime(utcDate.Year, utcDate.Month, day: 1, hour: 12, minute: 0, second: 0, kind: DateTimeKind.Utc ); + + case VolumeDetalizationLevel.Weekly: + var dt = DayOfWeek.Sunday - utcDate.DayOfWeek; + DateTime result = utcDate.AddDays(dt); + return new DateTime(result.Year, result.Month, result.Day, hour: 12, minute: 0, second: 0, kind: DateTimeKind.Utc ); + + case VolumeDetalizationLevel.Daily: + return new DateTime(utcDate.Year, utcDate.Month, utcDate.Day, hour: 12, minute: 0, second: 0, kind: DateTimeKind.Utc ); + + case VolumeDetalizationLevel.Hourly: + return new DateTime(utcDate.Year, utcDate.Month, utcDate.Day, utcDate.Hour, minute: 0, second: 0, kind: DateTimeKind.Utc ); + + case VolumeDetalizationLevel.Fractional: + var minute = utcDate.Minute; + minute -= (minute % FRACTIONAL_FREQUENCY_MINUTES); + return new DateTime(utcDate.Year, utcDate.Month, utcDate.Day, utcDate.Hour, minute: minute, second: 0, kind: DateTimeKind.Utc ); + + default: + throw new SocialException(StringConsts.TS_VOLUME_UNKNOWN_DETALIZATION_ERROR.Args(level)); + } + } + + /// + /// Maps the detalization level to minutes + /// + public static int MapDetalizationToMinutes(VolumeDetalizationLevel level) + { + switch(level) + { + case VolumeDetalizationLevel.Quarter: return 24*60*30*3; + case VolumeDetalizationLevel.Monthly: return 24*60*30; + case VolumeDetalizationLevel.Weekly: return 24*60*7; + case VolumeDetalizationLevel.Daily: return 24*60; + case VolumeDetalizationLevel.Hourly: return 60; + case VolumeDetalizationLevel.Fractional: return 5; + + default: + throw new SocialException(StringConsts.TS_VOLUME_UNKNOWN_DETALIZATION_ERROR.Args(level)); + } + } + + + protected Volume(TrendingSystemService director) : base(director) + { + } + #endregion + + #region Fields + private int m_HistoryLengthDays = MIN_HISTORY_LENGTH_DAYS; + private VolumeDetalizationLevel m_DetalizationLevel = VolumeDetalizationLevel.Daily; + private int m_OldDataPurgeFrequencyMin = MIN_OLD_DATA_PURGE_FREQUENCY_MINUTES; + private DateTime m_LastDeleteDate = DateTime.MinValue; + #endregion + + #region Properties + /// + /// Defines a point beyond which the data will be discarded + /// + [Config(Default = MIN_HISTORY_LENGTH_DAYS)] + public int HistoryLengthDays + { + get { return m_HistoryLengthDays; } + set + { + m_HistoryLengthDays = value < MIN_HISTORY_LENGTH_DAYS ? MIN_HISTORY_LENGTH_DAYS : value; + } + } + + [Config(Default = VolumeDetalizationLevel.Daily)] + public VolumeDetalizationLevel DetalizationLevel + { + get + { + return m_DetalizationLevel; + } + set + { + CheckServiceInactive(); + m_DetalizationLevel = value; + } + } + + [Config(Default = MIN_OLD_DATA_PURGE_FREQUENCY_MINUTES)] + [ExternalParameter] + public int OldDataPurgeFrequencyMin + { + get + { + return m_OldDataPurgeFrequencyMin; + } + set + { + CheckServiceInactive(); + m_OldDataPurgeFrequencyMin = + value < MIN_OLD_DATA_PURGE_FREQUENCY_MINUTES ? MIN_OLD_DATA_PURGE_FREQUENCY_MINUTES : value; + } + } + + public TrendingSystemHost TrendingHost {get { return this.ComponentDirector.TrendingHost; }} + + #endregion + + #region Public + /// + /// Saves gauge in volume + /// This method does not leak + /// This method is NOT thread-safe + /// + public void WriteGauge(SocialTrendingGauge gauge) + { + try + { + if (!Running) return; + DoWriteGauge(gauge); + } + catch (Exception e) + { + Log(MessageType.Error, "WriteGauge", e.ToMessageWithType(), e); + } + } + + /// + /// Load trending in volume + /// This method does not leak + /// + public List GetTreding(TrendingQuery query) + { + try + { + if (!Running) return new List(); + return DoGetTreding(query); + } + catch (Exception e) + { + Log(MessageType.Error, "GetTreding", e.ToMessageWithType(), e); + } + return new List(); + } + + /// + /// Delete trending in volume + /// + public void DeleteOldData(DateTime utcNow) + { + try + { + if (!Running) return; + var deletePoint = utcNow.AddDays(-m_HistoryLengthDays); + DoDeleteOldData(deletePoint); + } + catch (Exception e) + { + Log(MessageType.Error, GetType().Name+".DeleteOldData", e.ToMessageWithType(), e); + } + } + #endregion + + #region Protected + + protected override void DoAcceptManagerVisit(object manager, DateTime managerNow) + { + base.DoAcceptManagerVisit(manager, managerNow); + var oldDate = NFX.IntMath.ChangeByRndPct(OldDataPurgeFrequencyMin, 0.25f); + if ((managerNow - m_LastDeleteDate).TotalMinutes > oldDate ) + { + m_LastDeleteDate = managerNow; + DeleteOldData(managerNow); + } + + } + + protected abstract void DoDeleteOldData(DateTime deletePoint); + protected abstract void DoWriteGauge(SocialTrendingGauge gauge); + protected abstract List DoGetTreding(TrendingQuery query); + + protected void Log(MessageType type, string from, string text, Exception error = null, Guid? related = null) + { + var msg = new NFX.Log.Message + { + Type = type, + Topic = SysConsts.LOG_TOPIC_APP_MANAGEMENT, + From = "{0}.{1}".Args(GetType().FullName, from), + Text = text, + Exception = error + }; + + if (related.HasValue) msg.RelatedTo = related.Value; + + App.Log.Write( msg ); + } + + #endregion + } + +} \ No newline at end of file diff --git a/src/Agni.Social/Trending/SocialTrendingGauge.cs b/src/Agni.Social/Trending/SocialTrendingGauge.cs new file mode 100644 index 0000000..7f97b24 --- /dev/null +++ b/src/Agni.Social/Trending/SocialTrendingGauge.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using NFX.Instrumentation; +using NFX.Serialization.BSON; +using NFX.DataAccess.Distributed; +using NFX; + +namespace Agni.Social.Trending +{ + /// + /// Advances entity trending by the specified count of events + /// + [Serializable] + [BSONSerializable("BAC4E8C4-9ED2-49C4-A95D-17B6A402FA7E")] + public sealed class SocialTrendingGauge : LongGauge, ISocialLogic + { + public const string BSON_FLD_ENTITY = "ent"; + public const string BSON_FLD_G_SHARD = "g_s"; + public const string BSON_FLD_G_ENTITY = "g_e"; + public const string BSON_FLD_DIMS = "dims"; + + public const int MAX_ENTITY_NAME_LENGTH = 8; + public const int MAX_DIMENSION_LENGTH = 500; + + public static bool TryValidateEntityName(string entityName) + { + if (entityName.IsNullOrWhiteSpace()) return false; + if (entityName.Length > MAX_ENTITY_NAME_LENGTH) return false; + for (var i = 0; i < entityName.Length; i++) + { + var c = entityName[i]; + if ((c < 'A' || c > 'Z') && + (c < 'a' || c > 'z') && + (c < '0' || c > '9' || i == 0) && + (c != '_')) + return false; + } + return true; + } + + /// + /// Records the trending information. + /// + /// Entity type - what is trending + /// Sharding area key + /// GDID of the trending entity + /// Trending count + /// Dimensions vector in plain or laconic format + public static void Emit(string tEntity, GDID gShard, GDID gEntity, long count, string dimensions) + { + if (!TryValidateEntityName(tEntity)) + throw new SocialException(StringConsts.ARGUMENT_ERROR + "Emit(tEntity!Valid:'{0}')".Args(tEntity)); + + if (dimensions != null && dimensions.Length > MAX_DIMENSION_LENGTH) + throw new SocialException(StringConsts.ARGUMENT_ERROR + "Emit(dims to long)"); + + var inst = App.Instrumentation; + if (!inst.Enabled) return; + + var datum = new SocialTrendingGauge(count) + { + m_Entity = tEntity, + m_G_Shard = gShard, + m_G_Entity = gEntity, + m_Dimensions = dimensions + }; + + inst.Record(datum); + } + + private SocialTrendingGauge(long count) : base(null, count) + { + } + + + private string m_Entity; + private GDID m_G_Shard; + private GDID m_G_Entity; + + private string m_Dimensions; + + public override string Source + { + get + { + var sb = new StringBuilder(128); + sb.Append(m_Entity); sb.Append('|'); + sb.Append(m_G_Shard); sb.Append('|'); + sb.Append(m_G_Entity); sb.Append('|'); + sb.Append(m_Dimensions); + + return sb.ToString(); + } + } + + /// Returns entity type + public string Entity { get { return m_Entity;} } + + /// Returns entity sharding key + public GDID G_Shard { get { return m_G_Shard;} } + + /// Returns entity GDID + public GDID G_Entity { get { return m_G_Entity;} } + + /// + /// Dimensions used for classification. e.g. 'USA', 'Accounting' etc. + /// Use laconic to assign multiple. + /// ATTENTION! Making dimensions too detailed significantly increases the number of samples + /// that the system needs to process in real time. + /// ATTENTION! In a particular business system, if dimension is a vector, it must be an ordered tuple of attr/values of the fixed size + /// (e.g "category,class" and "class,category" are different dimensions) + /// + public string Dimensions { get { return m_Dimensions;} } + + public override string Description { get { return "Advances '{0}' social trending by the specified count of events".Args(Entity);}} + public override string ValueUnitName { get { return CoreConsts.UNIT_NAME_EVENT; }} + + protected override Datum MakeAggregateInstance() + { + var aggregated = new SocialTrendingGauge(this.Value) + { + m_Entity = this.m_Entity, + m_G_Shard = this.m_G_Shard, + m_G_Entity = this.m_G_Entity, + m_Dimensions = this.m_Dimensions + }; + return aggregated; + } + + public override void SerializeToBSON(BSONSerializer serializer, BSONDocument doc, IBSONSerializable parent, ref object context) + { + base.SerializeToBSON(serializer, doc, parent, ref context); + + doc.Add(BSON_FLD_ENTITY, m_Entity); + doc.Add(BSON_FLD_G_SHARD, m_G_Shard.ToString()); + doc.Add(BSON_FLD_G_ENTITY, m_G_Entity.ToString()); + doc.Add(BSON_FLD_DIMS, m_Dimensions); + } + + public override void DeserializeFromBSON(BSONSerializer serializer, BSONDocument doc, ref object context) + { + base.DeserializeFromBSON(serializer, doc, ref context); + + m_Entity = doc.TryGetObjectValueOf(BSON_FLD_ENTITY) .AsString(); + m_G_Shard = doc.TryGetObjectValueOf(BSON_FLD_G_SHARD) .AsGDID (); + m_G_Entity = doc.TryGetObjectValueOf(BSON_FLD_G_ENTITY) .AsGDID (); + m_Dimensions = doc.TryGetObjectValueOf(BSON_FLD_DIMS) .AsString(); + } + } +} diff --git a/src/Agni.Social/Trending/TrendingManager.cs b/src/Agni.Social/Trending/TrendingManager.cs new file mode 100644 index 0000000..de11523 --- /dev/null +++ b/src/Agni.Social/Trending/TrendingManager.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using NFX; +using NFX.ApplicationModel; +using NFX.Environment; + +using Agni.WebMessaging; +using Agni.Social.Trending.Server; +using Agni.Coordination; + +namespace Agni.Social.Trending +{ + public class TrendingManager : DisposableObject, IApplicationStarter, IApplicationFinishNotifiable + { + #region ctor + + private static object s_Lock = new object(); + private static volatile TrendingManager s_Instance; + + public static TrendingManager Instance + { + get + { + var instance = s_Instance; + if (instance == null) + throw new SocialException(StringConsts.TS_INSTANCE_DATA_LAYER_IS_NOT_ALLOCATED_ERROR.Args(typeof(TrendingSystemService).Name)); + return instance; + } + } + + private TrendingManager() + { + lock (s_Lock) + { + if (s_Instance != null) + throw new SocialException(StringConsts.TS_INSTANCE_ALREADY_ALLOCATED_ERROR.Args(GetType().Name)); + s_Instance = this; + } + } + + protected override void Destructor() + { + lock (s_Lock) + { + base.Destructor(); + s_Instance = null; + } + } + + #endregion + + #region fields + + private IConfigSectionNode m_Config; + private HostSet m_HostSet; + + #endregion + + #region properties + + public bool ApplicationStartBreakOnException {get { return true; } } + public string Name { get { return GetType().Name; } } + public HostSet HostSet { get { return m_HostSet; } } + + #endregion + + #region public + + public void Configure(IConfigSectionNode node) + { + m_Config = node; + } + + public void ApplicationFinishAfterCleanup(IApplication application) + { + } + + public void ApplicationFinishBeforeCleanup(IApplication application) + { + } + + public void ApplicationStartAfterInit(IApplication application) + { + var trendingHostSetAttr = m_Config.AttrByName(SocialConsts.CONFIG_TRENDING_HOST_SET_ATTR).Value; + m_HostSet = AgniSystem.ProcessManager.HostSets[trendingHostSetAttr]; + if (m_HostSet == null) throw new SocialException(StringConsts.TS_HOST_SET_NOT_CONFIG_ERROR.Args(SocialConsts.CONFIG_TRENDING_HOST_SET_ATTR)); + } + + public void ApplicationStartBeforeInit(IApplication application) + { + } + + // ITrendingSystem + + public IEnumerable GetTrending(TrendingQuery query) + { + var pair = HostSet.AssignHost(App.TimeSource.UTCNow.Ticks); + return Contracts.ServiceClientHub.CallWithRetry>(trending => trending.GetTrending(query), pair.Select(host => host.RegionPath)); + } + + #endregion + } +} diff --git a/src/Agni.Social/Trending/TrendingSystemClient.cs b/src/Agni.Social/Trending/TrendingSystemClient.cs new file mode 100644 index 0000000..b5fbc30 --- /dev/null +++ b/src/Agni.Social/Trending/TrendingSystemClient.cs @@ -0,0 +1,127 @@ +//Generated by Agni.Clients.Tools.AgniGluecCompiler + +/* Auto generated by Glue Client Compiler tool (gluec) +on 8/13/2017 3:50:49 PM at SEXTOD by Anton +Do not modify this file by hand if you plan to regenerate this file again by the tool as manual changes will be lost +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using NFX.Glue; +using NFX.Glue.Protocol; + + +namespace Agni.Social.Trending +{ +// This implementation needs @Agni.@Social.@Trending.@ITrendingSystemClient, so +// it can be used with ServiceClientHub class + + /// + /// Client for glued contract Agni.Social.Trending.ITrendingSystem server. + /// Each contract method has synchronous and asynchronous versions, the later denoted by 'Async_' prefix. + /// May inject client-level inspectors here like so: + /// client.MsgInspectors.Register( new YOUR_CLIENT_INSPECTOR_TYPE()); + /// + public class TrendingSystemClient : ClientEndPoint, @Agni.@Social.@Trending.@ITrendingSystemClient + { + + #region Static Members + + private static TypeSpec s_ts_CONTRACT; + private static MethodSpec @s_ms_Send_0; + private static MethodSpec @s_ms_GetTrending_1; + + //static .ctor + static TrendingSystemClient() + { + var t = typeof(@Agni.@Social.@Trending.@ITrendingSystem); + s_ts_CONTRACT = new TypeSpec(t); + @s_ms_Send_0 = new MethodSpec(t.GetMethod("Send", new Type[]{ typeof(@Agni.@Social.@Trending.@SocialTrendingGauge[]) })); + @s_ms_GetTrending_1 = new MethodSpec(t.GetMethod("GetTrending", new Type[]{ typeof(@Agni.@Social.@Trending.@TrendingQuery) })); + } + #endregion + + #region .ctor + public TrendingSystemClient(string node, Binding binding = null) : base(node, binding) { ctor(); } + public TrendingSystemClient(Node node, Binding binding = null) : base(node, binding) { ctor(); } + public TrendingSystemClient(IGlue glue, string node, Binding binding = null) : base(glue, node, binding) { ctor(); } + public TrendingSystemClient(IGlue glue, Node node, Binding binding = null) : base(glue, node, binding) { ctor(); } + + //common instance .ctor body + private void ctor() + { + + } + + #endregion + + public override Type Contract + { + get { return typeof(@Agni.@Social.@Trending.@ITrendingSystem); } + } + + + + #region Contract Methods + + /// + /// Synchronous invoker for 'Agni.Social.Trending.ITrendingSystem.Send'. + /// This is a one-way call per contract specification, meaning - the server sends no acknowledgement of this call receipt and + /// there is no result that server could return back to the caller. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// + public void @Send(@Agni.@Social.@Trending.@SocialTrendingGauge[] @gauges) + { + var call = Async_Send(@gauges); + if (call.CallStatus != CallStatus.Dispatched) + throw new ClientCallException(call.CallStatus, "Call failed: 'TrendingSystemClient.Send'"); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Trending.ITrendingSystem.Send'. + /// This is a one-way call per contract specification, meaning - the server sends no acknowledgement of this call receipt and + /// there is no result that server could return back to the caller. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg. + /// + public CallSlot Async_Send(@Agni.@Social.@Trending.@SocialTrendingGauge[] @gauges) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_Send_0, true, RemoteInstance, new object[]{@gauges}); + return DispatchCall(request); + } + + + + /// + /// Synchronous invoker for 'Agni.Social.Trending.ITrendingSystem.GetTrending'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning '@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Trending.@TrendingEntity>' or WrappedExceptionData instance. + /// ClientCallException is thrown if the call could not be placed in the outgoing queue. + /// RemoteException is thrown if the server generated exception during method execution. + /// + public @System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Trending.@TrendingEntity> @GetTrending(@Agni.@Social.@Trending.@TrendingQuery @query) + { + var call = Async_GetTrending(@query); + return call.GetValue<@System.@Collections.@Generic.@IEnumerable<@Agni.@Social.@Trending.@TrendingEntity>>(); + } + + /// + /// Asynchronous invoker for 'Agni.Social.Trending.ITrendingSystem.GetTrending'. + /// This is a two-way call per contract specification, meaning - the server sends the result back either + /// returning no exception or WrappedExceptionData instance. + /// CallSlot is returned that can be queried for CallStatus, ResponseMsg and result. + /// + public CallSlot Async_GetTrending(@Agni.@Social.@Trending.@TrendingQuery @query) + { + var request = new RequestAnyMsg(s_ts_CONTRACT, @s_ms_GetTrending_1, false, RemoteInstance, new object[]{@query}); + return DispatchCall(request); + } + + + #endregion + + }//class +}//namespace diff --git a/src/Agni.Social/Trending/TrendingSystemInstrumentationProvider.cs b/src/Agni.Social/Trending/TrendingSystemInstrumentationProvider.cs new file mode 100644 index 0000000..ba96195 --- /dev/null +++ b/src/Agni.Social/Trending/TrendingSystemInstrumentationProvider.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using NFX; +using NFX.Environment; +using NFX.Instrumentation; + +using Agni.Contracts; +using Agni.Coordination; + + +namespace Agni.Social.Trending +{ + /// + /// Uploads trending data to the to the trending system server. + /// This provider only processes SocialTrendingGauge instances and ignores others. + /// Use this provider with CompositeProvider when configuring application setting instrumentation + /// + public sealed class TrendingSystemInstrumentationProvider : InstrumentationProvider + { + #region CONSTS + public const int HIGH_PASS_FILTER_COUNTER_MASK = 0x0ffff; + public const int HIGH_PASS_FILTER_TIME_WINDOW_MINUTES_MIN = 1; + public const int HIGH_PASS_FILTER_TIME_WINDOW_MINUTES_DEFAULT = 60; + public const int HIGH_PASS_FILTER_BYPASS_THRESHOLD_COUNT_DEFAULT = 10; + #endregion + + //we store COUNTER_MASK (65K) GDID latches per entity, this way we can roughly filter-out traffic which happens not-frequently + // suppose there are 1,000,000,000 trending users / 0xffff = 15,258 collisions per 1 filter cell + private class entityFilter : Dictionary, INamed + { + public entityFilter(string name) : base(HIGH_PASS_FILTER_COUNTER_MASK) { m_Name = name;} + private string m_Name; + public string Name { get { return m_Name;} } + } + + + public TrendingSystemInstrumentationProvider(InstrumentationService director) : base(director) + { + } + + #region Fields + private string m_HostsetName; + private HostSet m_Hostset; + + private Registry m_HighPassFilter = new Registry(); + private bool m_HighPassFilterEnabled; + private int m_HighPassFilterTimeWindowMinutes = HIGH_PASS_FILTER_TIME_WINDOW_MINUTES_DEFAULT; + private int m_HighPassFilterBypassThresholdCount = HIGH_PASS_FILTER_BYPASS_THRESHOLD_COUNT_DEFAULT; + #endregion + + #region Properties + /// + /// Provides the name of hostset where the data will be sent + /// + [Config(path:"$hostset|$host-set|$hostset-name")] + public string HostsetName + { + get { return m_HostsetName; } + set + { + CheckServiceInactive(); + m_HostsetName = value; + } + } + + public HostSet HostSet + { + get + { + if(m_Hostset == null) m_Hostset = AgniSystem.ProcessManager.HostSets[m_HostsetName]; + return m_Hostset; + } + } + + /// + /// Enables high-pass-filter that filters-out datums that do not happen frequently enough + /// + [ExternalParameter] + [Config] + public bool HighPassFilterEnabled + { + get { return m_HighPassFilterEnabled; } + set { m_HighPassFilterEnabled = value; } + } + + /// + /// Specifies the time window size in minutes within which some activity has to take place, otherwise the slot get discarded + /// + [ExternalParameter] + [Config(Default = HIGH_PASS_FILTER_TIME_WINDOW_MINUTES_DEFAULT)] + public int HighPassFilterTimeWindowMinutes + { + get { return m_HighPassFilterTimeWindowMinutes; } + set + { + m_HighPassFilterTimeWindowMinutes = value < HIGH_PASS_FILTER_TIME_WINDOW_MINUTES_MIN ? m_HighPassFilterTimeWindowMinutes : value; + } + } + + /// + /// Specifies the threshold value above which the data bypasses filter + /// + [ExternalParameter] + [Config(Default = HIGH_PASS_FILTER_BYPASS_THRESHOLD_COUNT_DEFAULT)] + public int HighPassFilterBypassThresholdCount + { + get { return m_HighPassFilterBypassThresholdCount; } + set + { + m_HighPassFilterBypassThresholdCount = value > 0 ? value : 0; + } + } + #endregion + + #region Protected + protected override void DoStart() + { + base.DoStart(); + m_Hostset = AgniSystem.ProcessManager.HostSets[m_HostsetName]; + m_HighPassFilter.Clear(); + } + + protected override void DoWaitForCompleteStop() + { + base.DoWaitForCompleteStop(); + m_HighPassFilter.Clear(); + } + + + protected override object BeforeBatch() + { + return new List(); + } + + protected override void AfterBatch(object batchContext) + { + var datumList = batchContext as List; + if (datumList != null) + send(datumList.ToArray()); + } + + protected override void Write(Datum aggregatedDatum, object batchContext, object typeContext) + { + var astg = aggregatedDatum as SocialTrendingGauge; + if (astg==null) return; + + if (!highPassFilter(astg)) return;//todo Instrument - the datum is not frequent enough and gets filtered-out + + var datumList = batchContext as List; + if (datumList != null) + { + datumList.Add(astg); + if (datumList.Count>100) + { + send(datumList.ToArray()); + datumList.Clear(); + } + } + else + send(astg); + } + #endregion + + #region pvt + private bool highPassFilter(SocialTrendingGauge sample) + { + if (!m_HighPassFilterEnabled) return true; + var entityName = sample.Entity; + if (entityName.IsNullOrWhiteSpace()) entityName = "*"; + + var key = sample.G_Entity.Counter & HIGH_PASS_FILTER_COUNTER_MASK; + DateTime lastDate; + var now = App.TimeSource.UTCNow; + + //If the sample happened more than threshold then bypass filtering by time altogether + var pass = sample.Value >= m_HighPassFilterBypassThresholdCount; + + var filter = m_HighPassFilter.GetOrRegister(entityName, n => new entityFilter(n), entityName); + + //only include those GDIDS that had some traffic in the past m_HighPassFilterTimeWindowMinutes + if (!pass && filter.TryGetValue(key, out lastDate)) + pass = (now-lastDate).TotalMinutes <= m_HighPassFilterTimeWindowMinutes; + + filter[key] = now; + + return pass; + } + + private void send(params SocialTrendingGauge[] data) + { + if (data==null || data.Length==0) return; + + var hostPair = HostSet.AssignHost(App.TimeSource.UTCNow.Ticks); + ServiceClientHub.CallWithRetry + ( + cl => cl.Send( data ), + hostPair.Select(host => host.RegionPath) + ); + } + #endregion + } +} diff --git a/src/Agni.Social/post-build b/src/Agni.Social/post-build new file mode 100644 index 0000000..69d2e30 --- /dev/null +++ b/src/Agni.Social/post-build @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +SCRIPT=`realpath -s $0` +SCRIPTPATH=`dirname $SCRIPT` + +SOLUTION_DIR=$1 +PROJECT_DIR=${SCRIPTPATH}/ + + +mono rsc "${PROJECT_DIR}/Graph/Server/Data/Schema/graph-node.rschema" -o out-name-prefix="Graph.Node." domain-search-paths="Agni.Social.Graph.Server.Data.Schema.*, Agni.Social" +mono rsc "${PROJECT_DIR}/Graph/Server/Data/Schema/graph-comment.rschema" -o out-name-prefix="Graph.Comment." domain-search-paths="Agni.Social.Graph.Server.Data.Schema.*, Agni.Social" diff --git a/src/Agni.Social/post-build.cmd b/src/Agni.Social/post-build.cmd new file mode 100644 index 0000000..f33d2d6 --- /dev/null +++ b/src/Agni.Social/post-build.cmd @@ -0,0 +1,13 @@ +set SOLUTION_DIR=%1 +set CONFIG=%2 +set PROJECT_DIR=%~dp0 +set TOOL_DIR=%SOLUTION_DIR%\lib\nfx\run-netf\ +set OUT_DIR=%SOLUTION_DIR%..\out\%CONFIG%\ + +copy /Y "%TOOL_DIR%rsc.exe" "%OUT_DIR%" +copy /Y "%TOOL_DIR%NFX.PAL.NetFramework.dll" "%OUT_DIR%" +copy /Y "%TOOL_DIR%NFX.Tools.dll" "%OUT_DIR%" + + +"%OUT_DIR%rsc" "%PROJECT_DIR%\Graph\Server\Data\Schema\graph-node.rschema" -o out-name-prefix="Graph.Node." domain-search-paths="Agni.Social.Graph.Server.Data.Schema.*, Agni.Social" +"%OUT_DIR%rsc" "%PROJECT_DIR%\Graph\Server\Data\Schema\graph-comment.rschema" -o out-name-prefix="Graph.Comment." domain-search-paths="Agni.Social.Graph.Server.Data.Schema.*, Agni.Social" diff --git a/src/Agni.Social/pre-build b/src/Agni.Social/pre-build new file mode 100644 index 0000000..9507682 --- /dev/null +++ b/src/Agni.Social/pre-build @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +SCRIPT=`realpath -s $0` +SCRIPTPATH=`dirname $SCRIPT` + +SOLUTION_DIR=$1 +PROJECT_DIR=${SCRIPTPATH}/ + +mono buildinfo.exe > "${PROJECT_DIR}BUILD_INFO.txt" +# mono ntc.exe "${PROJECT_DIR}/WebManager/Pages/*.nht" -r -ext ".auto.cs" -src diff --git a/src/Agni.Social/pre-build.cmd b/src/Agni.Social/pre-build.cmd new file mode 100644 index 0000000..c871917 --- /dev/null +++ b/src/Agni.Social/pre-build.cmd @@ -0,0 +1,7 @@ +set SOLUTION_DIR=%1 +set CONFIG=%2 +set PROJECT_DIR=%~dp0 +set TOOL_DIR=%SOLUTION_DIR%\lib\nfx\run-netf\ + + +"%TOOL_DIR%buildinfo" > "%PROJECT_DIR%BUILD_INFO.txt"