/// <summary> /// Attempt to get a lock for updating a persiston. Fails if another user has it locked already. /// Succeeds efficiently if this user already had it locked. /// </summary> /// <returns>success flag and error reason code</returns> public (bool, string) RequestLock(DatonKey datonKey, string version, string sessionKey) { //check if already locked by this server if (Locks.TryGetValue(datonKey, out LockInfo linfo)) { bool isLockedByMe = linfo.SessionKey == sessionKey; if (!isLockedByMe) { return(false, Constants.ERRCODE_LOCK); //someone else on this server has it locked } return(true, null); //this session already has it locked } //attempt lock on database using (var lockdb = GetLockConnection()) { if (RetroLock.Lock(lockdb, datonKey, version, sessionKey)) { //success Locks[datonKey] = new LockInfo { SessionKey = sessionKey, DatonKey = datonKey, OldVersion = version }; return(true, null); } //failed, so determine why (string verifiedVersion, _) = RetroLock.GetVersion(lockdb, datonKey); if (verifiedVersion != version) { return(false, Constants.ERRCODE_VERSION); //the most recent version is newer than the version known by the caller } return(false, Constants.ERRCODE_LOCK); //someone else on another server has it locked } }
/// <summary> /// Attempt to release a lock /// </summary> /// <returns>success flag and new version number (new version only provided if persiston was actually written)</returns> public (bool, string) ReleaseLock(DatonKey datonKey, string sessionKey) { //check if it is possible to unlock if (Locks.TryGetValue(datonKey, out LockInfo linfo)) { bool isLockedByMe = linfo.SessionKey == sessionKey; if (!isLockedByMe) { return(false, null); //can't unlock because someone else on this server has it locked } } else { return(false, null); //can't unlock because it is not locked by anyone on this server } //attempt unlock on database using (var lockdb = GetLockConnection()) { (bool unlockOK, string newVersion) = RetroLock.Unlock(lockdb, datonKey, sessionKey, linfo.WasWritten, ServerLifeNumber); Locks.TryRemove(datonKey, out _); //we forget in memory even if there was a database problem (should not happen) //propogate change (this is hooked up to code that pushes the change to subscribed sessions) if (linfo.WasWritten) { ChangePropogator?.Invoke(datonKey, newVersion); } return(unlockOK, newVersion); } }
/// <summary> /// Get the version of the persiston. This ALWAYS goes to the database so don't call this too much. /// The result is guaranteed and will assign a version if there was none recorded. /// </summary> public string GetVersion(DatonKey datonKey) { using (var lockdb = GetLockConnection()) { (string version, _) = RetroLock.GetVersion(lockdb, datonKey); return(version); } }
/// <summary> /// Get the state of a daton lock. This only checks the local server, so the daton could be locked even if it returns /// a value indicating not locked. /// </summary> /// <returns>3 values: is locked at all; is locked by the given session; version number when locked</returns> public (bool, bool, string) GetLockState(DatonKey datonKey, string sessionKey) { if (Locks.TryGetValue(datonKey, out LockInfo linfo)) { bool isLockedByMe = linfo.SessionKey == sessionKey; return(true, isLockedByMe, linfo.OldVersion); } return(false, false, null); }
/// <summary> /// Notify the lock system that a daton was written to the database. This does not unlock it, but it changes the behavior when unlocked. /// </summary> /// <returns>true if successful</returns> public bool NotifyDatonWritten(DatonKey datonKey) { if (Locks.TryGetValue(datonKey, out LockInfo linfo)) { linfo.WasWritten = true; return(true); } return(false); }
/// <summary> /// Update the touched column for the daton, only if locked by the given session. /// </summary> public static void Touch(DbConnection db, DatonKey key, string sessionKey) { using (var cmd = db.CreateCommand()) { cmd.CommandText = "update RetroLock set Touched=@t where DatonKey=@k and LockedBy=@s"; Utils.AddParameterWithValue(cmd, "t", DateTime.UtcNow); Utils.AddParameterWithValue(cmd, "k", key.ToString()); Utils.AddParameterWithValue(cmd, "s", sessionKey); cmd.ExecuteNonQuery(); } }
/// <summary> /// Get where clause for main table, or null if none /// </summary> protected SqlSelectBuilder.Where MainTableWhereClause(TableDef tabledef, DatonKey key) { if (key is PersistonKey pkey) { return(MainTableWhereClause(tabledef, pkey)); } if (key is ViewonKey vkey) { return(MainTableWhereClause(tabledef, vkey)); } return(null); }
public Daton Get(DatonKey key) { if (Cache.TryGetValue(key, out Item i)) { if (key is PersistonKey) //don't keep viewons in cache very long { i.LastAccessedUtc = DateTime.UtcNow; } return(i.Daton); } return(null); }
/// <summary> /// Attempt to obtain a lock /// </summary> /// <param name="version">the version which was known</param> /// <param name="sessionKey"></param> /// <returns>true if successful</returns> public static bool Lock(DbConnection db, DatonKey key, string version, string sessionKey) { //assuming first there is a record, attempt to get the lock by updating it using (var cmd = db.CreateCommand()) { cmd.CommandText = "update RetroLock set Touched=@t, LockedBy=@s where DatonKey=@k and DatonVersion=@v and (LockedBy is null or Touched<@old)"; Utils.AddParameterWithValue(cmd, "t", DateTime.UtcNow); Utils.AddParameterWithValue(cmd, "s", sessionKey); Utils.AddParameterWithValue(cmd, "k", key.ToString()); Utils.AddParameterWithValue(cmd, "v", version); Utils.AddParameterWithValue(cmd, "old", DateTime.UtcNow.AddSeconds(-120)); int nrows = cmd.ExecuteNonQuery(); if (nrows == 1) { return(true); } } //update touched date: this tells us if the record exists, and ensures it won't be cleaned up during the lock process; //also unlock it if the lock is too old //bool recordExists; //using (var cmd = db.CreateCommand()) //{ // cmd.CommandText = "update RetroLock set LockedBy=(case when Touched<@old then null else LockedBy end), Touched=@t where DatonKey=@k"; // Utils.AddParameterWithValue(cmd, "old", DateTime.UtcNow.AddSeconds(-120)); // Utils.AddParameterWithValue(cmd, "t", DateTime.UtcNow); // Utils.AddParameterWithValue(cmd, "k", key.ToString()); // int nrows = cmd.ExecuteNonQuery(); // recordExists = nrows == 1; //} //reached here, so there was no record; create one with lock //(this won't occur if everything is working, since reading the version should happen before attempting to lock) using (var cmd = db.CreateCommand()) { cmd.CommandText = "insert into RetroLock (DatonKey,DatonVersion,Touched,LockedBy) values(@k,@v,@t,@s)"; Utils.AddParameterWithValue(cmd, "k", key.ToString()); Utils.AddParameterWithValue(cmd, "v", version); Utils.AddParameterWithValue(cmd, "t", DateTime.UtcNow); Utils.AddParameterWithValue(cmd, "s", sessionKey); try { cmd.ExecuteNonQuery(); return(true); } catch { return(false); //another user created the lock record since we queried it above } } }
/// <summary> /// Determine if any subscription from any client is out of date; that is, if there is a client /// whose latest version is not the new version provided for the daton key provided. /// </summary> public bool IsAnyOutOfDate(DatonKey key, string newVersion) { foreach (var cli in Sessions.Values) { if (!cli.Subscriptions.TryGetValue(key, out string clientsVersion)) { continue; } if (clientsVersion != newVersion) { return(true); } } return(false); }
/// <summary> /// Given parsed untyped JSON in full compatible format, construct it into a typed daton /// </summary> public static Daton FromCompatibleWireFull(DataDictionary dbdef, JObject jroot) { var datonKey = DatonKey.Parse(jroot.Value <string>("key")); var datondef = dbdef.FindDef(datonKey); var daton = Utils.ConstructDaton(datondef.Type, datondef); daton.Key = datonKey; daton.Version = jroot.Value <string>("version"); if (daton is Viewon viewon && jroot.Value <bool>("isComplete") == false) { viewon.IsCompleteLoad = false; } var mainRowsNode = jroot[CamelCasify(datondef.MainTableDef.Name)]; if (mainRowsNode == null) { return(daton); } if (!(mainRowsNode is JArray mainRowsArray)) { throw new Exception($"{datondef.MainTableDef.Name} node must be an array"); } if (datondef.MultipleMainRows) { var targetListInfo = datondef.Type.GetField(datondef.MainTableDef.Name); if (targetListInfo == null) { throw new Exception($"Expected {datondef.MainTableDef.Name} to be a member of {datondef.Type.Name}"); } var targetList = Utils.CreateOrGetFieldValue <IList>(daton, targetListInfo) as IList; ReadCompatibleJsonRowArray(mainRowsArray, datondef.MainTableDef, (IList)targetList); } else { if (mainRowsArray.Count != 1) { throw new Exception($"{datondef.MainTableDef.Name} node must have one element for this daton type"); } ReadCompatibleJsonRow(mainRowsArray[0] as JObject, datondef.MainTableDef, daton); } daton.Recompute(datondef); return(daton); }
/// <summary> /// Get the daton version code and session key holding the lock if any; if the lock row was missing, create it and assign /// the version /// </summary> /// <returns>(version,lockedBy) where lockedBy might be null</returns> public static (string, string) GetVersion(DbConnection db, DatonKey key) { retry: //read existing row using (var cmd = db.CreateCommand()) { cmd.CommandText = "select DatonVersion,LockedBy from RetroLock where DatonKey=@k"; var p = cmd.CreateParameter(); p.ParameterName = "k"; p.Value = key.ToString(); cmd.Parameters.Add(p); using (var rdr = cmd.ExecuteReader()) { if (rdr.Read()) { return(Utils.Read <string>(rdr, 0), Utils.Read <string>(rdr, 1)); } } } //not found, so create it string version = Guid.NewGuid().ToString(); using (var cmd = db.CreateCommand()) { try { cmd.CommandText = "insert into RetroLock (DatonKey,DatonVersion,Touched) values(@k,@v,@t)"; Utils.AddParameterWithValue(cmd, "k", key.ToString()); Utils.AddParameterWithValue(cmd, "v", version); Utils.AddParameterWithValue(cmd, "t", DateTime.UtcNow); cmd.ExecuteNonQuery(); return(version, null); } catch { //rare failure: another user created the row between when we queried and attempted the insert Thread.Sleep(1); goto retry; } } }
/// <summary> /// Change the subscription state of a daton for a client /// </summary> public void ManageSubscribe(string sessionKey, DatonKey datonKey, string version, bool subscribe) { if (datonKey.IsNew) { throw new Exception("Cannot subscribe to an unsaved persiston"); } if (!Sessions.TryGetValue(sessionKey, out var ses)) { return; } if (subscribe) { ses.Subscriptions[datonKey] = version; } else { ses.Subscriptions.TryRemove(datonKey, out _); } }
/// <summary> /// Get the list of daton keys with version numbers that were updated by other servers since the given time /// </summary> /// <param name="serverLifeNumber">the number for this server; updates from this server are excluded from the query</param> public static List <(DatonKey, string)> GetRecentUpdatesByOtherServers(DbConnection db, DateTime sinceUtc, int serverLifeNumber) { var ret = new List <(DatonKey, string)>(); using (var cmd = db.CreateCommand()) { cmd.CommandText = $"select DatonKey,DatonVersion from RetroLock where Touched>@t and UpdatedByServer<>@u"; Utils.AddParameterWithValue(cmd, "t", sinceUtc); Utils.AddParameterWithValue(cmd, "u", serverLifeNumber); using (var rdr = cmd.ExecuteReader()) { while (rdr.Read()) { var key = DatonKey.Parse(Utils.Read <string>(rdr, 0)); string version = Utils.Read <string>(rdr, 1); ret.Add((key, version)); } } } return(ret); }
/// <summary> /// Unlock a daton and optionally assign new version; only has an effect if it was locked by the given session /// </summary> /// <param name="datonWasWritten">pass true if this unlock is following a write, or false if it is an abandoned lock</param> /// <returns>success flag and the new version (version only returned if datonWasWritten)</returns> public static (bool, string) Unlock(DbConnection db, DatonKey key, string sessionKey, bool datonWasWritten, int serverLifeNumber) { string version = datonWasWritten ? Guid.NewGuid().ToString() : null; using (var cmd = db.CreateCommand()) { string versionsql = datonWasWritten ? ",DatonVersion=@v,UpdatedByServer=@u" : ""; cmd.CommandText = $"update RetroLock set LockedBy=null,Touched=@t{versionsql} where DatonKey=@k and LockedBy=@s"; Utils.AddParameterWithValue(cmd, "t", DateTime.UtcNow); Utils.AddParameterWithValue(cmd, "k", key.ToString()); Utils.AddParameterWithValue(cmd, "s", sessionKey); if (datonWasWritten) { Utils.AddParameterWithValue(cmd, "v", version); Utils.AddParameterWithValue(cmd, "u", serverLifeNumber); } int nrows = cmd.ExecuteNonQuery(); bool success = nrows == 1; return(success, version); } }
/// <summary> /// Given parsed untyped JSON in diff format, construct a PersistonDiff /// </summary> public static PersistonDiff FromDiff(DataDictionary dbdef, JObject jroot) { var datonKey = DatonKey.Parse(jroot.Value <string>("key")); var datondef = dbdef.FindDef(datonKey); var diff = new PersistonDiff(datondef, datonKey, jroot.Value <string>("version")); ReadJsonDiffRowArray(jroot, datondef.MainTableDef, diff.MainTable); //existing single-main-row diffs might not include the primary key in the main row, so add it here if (!datondef.MultipleMainRows) { var mainDiffRow = diff.MainTable.First(); if (!mainDiffRow.Columns.ContainsKey(datondef.MainTableDef.PrimaryKeyColName)) { var pkColdef = datondef.MainTableDef.FindCol(datondef.MainTableDef.PrimaryKeyColName); var pk = Utils.ChangeType(((PersistonKey)datonKey).PrimaryKey, pkColdef.CSType); mainDiffRow.Columns[datondef.MainTableDef.PrimaryKeyColName] = pk; } } return(diff); }
/// <summary> /// Get a daton, from cache or load from database. The reutrn value is a shared instance so the caller may not modify it. /// For new unsaved persistons with -1 as the key, this will create the instance with default values. /// </summary> /// <param name="user">if null, the return value is a shared guaranteed complete daton; if user is provided, /// the return value may be a clone with some rows removed or columns set to null</param> /// <param name="forceCheckLatest">if true then checks database to ensure latest version even if it was cached</param> /// <returns>object with daton, or readable errors</returns> public async Task <RetroSql.LoadResult> GetDaton(DatonKey key, IUser user, bool forceCheckLatest = false) { //new persiston: return now if (key.IsNew) { var datondef2 = DataDictionary.FindDef(key); Daton newDaton = Utils.Construct(datondef2.Type) as Daton; Utils.FixTopLevelDefaultsInNewPersiston(datondef2, newDaton); newDaton.Key = key; if (datondef2.Initializer != null) { await datondef2.Initializer(newDaton); } return(new RetroSql.LoadResult { Daton = newDaton }); } //get from cache if possible, and optionally ignore cached version if it is not the latest string verifiedVersion = null; Daton daton = DatonCache.Get(key); if (forceCheckLatest && daton != null) { //viewons: always ignore cache; persistons: use cached only if known to be latest if (daton is Persiston) { verifiedVersion = LockManager.GetVersion(key); if (verifiedVersion != daton.Version) { daton = null; } } else { daton = null; } } //get from database if needed (and cache it), or abort var datondef = DataDictionary.FindDef(key); if (typeof(Persiston).IsAssignableFrom(datondef.Type) && (key is ViewonKey)) { throw new Exception("Persiston requested but key format is for viewon"); } if (typeof(Viewon).IsAssignableFrom(datondef.Type) && (key is PersistonKey)) { throw new Exception("Viewon requested but key format is for persiston"); } if (daton == null) { var sql = GetSqlInstance(key); RetroSql.LoadResult loadResult; using (var db = GetDbConnection(datondef.DatabaseNumber)) loadResult = await sql.Load(db, DataDictionary, user, key, ViewonPageSize); if (loadResult.Daton == null) { return(loadResult); } daton = loadResult.Daton; if (verifiedVersion == null && daton is Persiston) { verifiedVersion = LockManager.GetVersion(key); } daton.Version = verifiedVersion; DatonCache.Put(daton); Diagnostics.IncrementLoadCount(); } //enforce permissions on the user if (user != null) { daton = daton.Clone(datondef); var guard = new SecurityGuard(DataDictionary, user); guard.HidePrivateParts(daton); } return(new RetroSql.LoadResult { Daton = daton }); }
private async Task HandleHttpMain(MainRequest req, IUser user, MainResponse resp) { //initialize if (req.Initialze != null) { resp.DataDictionary = Retrovert.DataDictionaryToWire(DataDictionary, user, LanguageMessages); } //load datons if (req.GetDatons != null) { var getResponses = new List <GetDatonResponse>(); foreach (var drequest in req.GetDatons) { var loadResult = await GetDaton(DatonKey.Parse(drequest.Key), user, forceCheckLatest : drequest.ForceLoad); var getResponse = new GetDatonResponse { Errors = loadResult.Errors }; if (loadResult.Daton != null) //null means it was not found by key, usually { bool doReturnToCaller = loadResult.Daton.Version == null || drequest.KnownVersion != loadResult.Daton.Version; //omit if client already has the current version if (doReturnToCaller) { getResponse.CondensedDaton = new CondensedDatonResponse { CondensedDatonJson = Retrovert.ToWire(DataDictionary, loadResult.Daton, false) }; } if (drequest.DoSubscribe && loadResult.Daton is Persiston) { ClientPlex.ManageSubscribe(req.SessionKey, loadResult.Daton.Key, loadResult.Daton.Version, true); } } else { getResponse.Key = drequest.Key; //only needed if daton is not returned to client } getResponses.Add(getResponse); } resp.GetDatons = getResponses.ToArray(); } //save datons if (req.SaveDatons != null) { var diffs = new List <PersistonDiff>(); foreach (var saveRequest in req.SaveDatons) { var diff = Retrovert.FromDiff(DataDictionary, saveRequest); diffs.Add(diff); } (bool success, var results) = await SaveDatons(req.SessionKey, user, diffs.ToArray()); var saveResponses = new List <SavePersistonResponse>(); foreach (var result in results) { saveResponses.Add(new SavePersistonResponse { IsDeleted = result.IsDeleted, IsSuccess = result.IsSuccess, OldKey = result.OldKey.ToString(), NewKey = result.NewKey?.ToString(), Errors = result.Errors }); } resp.SavedPersistons = saveResponses.ToArray(); resp.SavePersistonsSuccess = success; } //change datons state if (req.ManageDatons != null) { var manageResponses = new List <ManageDatonResponse>(req.ManageDatons.Length); foreach (var mrequest in req.ManageDatons) { //what does the caller wants to change? var datonKey = DatonKey.Parse(mrequest.Key); bool wantsLock = mrequest.SubscribeState == 2; bool wantsSubscribe = mrequest.SubscribeState >= 1; //handle change in subscription //(Performance note: unsubscribe should happen before unlock so that the unlock-propogation can short circuit reloading. Ultimately //if only one client is dealing with a daton and that client releases the lock and subscription, this server can forget about it //immediately.) bool isSubscribed = false; if (datonKey is PersistonKey) { ClientPlex.ManageSubscribe(req.SessionKey, datonKey, mrequest.Version, wantsSubscribe); isSubscribed = wantsSubscribe; } //handle change in lock string lockErrorCode = ""; bool hasLock = false; if (wantsLock) { if (string.IsNullOrEmpty(mrequest.Version)) { throw new Exception("Version required to lock daton"); } (hasLock, lockErrorCode) = LockManager.RequestLock(datonKey, mrequest.Version, req.SessionKey); } else { LockManager.ReleaseLock(datonKey, req.SessionKey); } manageResponses.Add(new ManageDatonResponse { ErrorCode = lockErrorCode, Key = mrequest.Key, SubscribeState = hasLock ? 2 : (isSubscribed ? 1 : 0) }); } resp.ManageDatons = manageResponses.ToArray(); } //quit - free up locks and memory if (req.DoQuit) { ClientPlex.DeleteSession(req.SessionKey); LockManager.ReleaseLocksForSession(req.SessionKey); } }
/// <summary> /// Load daton from database. Caller is responsible for setting the version (this does not deal with locks or versions) /// </summary> /// <param name="pageSize">only inspected for viewons main table</param> /// <returns>null if not found</returns> public virtual async Task <LoadResult> Load(IDbConnection db, DataDictionary dbdef, IUser user, DatonKey key, int pageSize) { var datondef = dbdef.FindDef(key); //viewon validation if (key is ViewonKey vkey2) { var validator = new Validator(user); await validator.ValidateCriteria(datondef, vkey2); if (validator.Errors.Any()) { return new LoadResult { Errors = validator.Errors.ToArray() } } ; } //handle viewon paging and ordering string sortColName = datondef.MainTableDef.DefaulSortColName; int pageNo = 0; if (key is ViewonKey vkey) { pageNo = vkey.PageNumber; if (vkey.SortColumnName != null) { sortColName = vkey.SortColumnName; } } else { pageSize = 0; //prohibit paging in persistons } //load main table var whereClause = MainTableWhereClause(datondef.MainTableDef, key); var loadResult = await LoadTable(db, dbdef, datondef.MainTableDef, whereClause, sortColName, pageSize, pageNo); loadResult.RowsByParentKey.TryGetValue("", out var rowsForParent); //single-main-row datons cannot have zero main rows if (!datondef.MultipleMainRows && (rowsForParent == null || rowsForParent.Count == 0)) { return(null); //was throw new Exception("Single-main-row not found using: " + whereClause.ToString()); } Daton daton = Utils.Construct(datondef.Type) as Daton; if (datondef.MultipleMainRows) { if (daton is Viewon viewon) { viewon.IsCompleteLoad = loadResult.IsComplete; } //copy rowsDict into daton's main table IList var listField = datondef.Type.GetField(datondef.MainTableDef.Name); if (rowsForParent != null) { var list = Utils.CreateOrGetFieldValue <IList>(daton, listField); foreach (var row in rowsForParent) { list.Add(row); } } } else //single main row { if (rowsForParent != null) { daton = rowsForParent?[0] as Daton; } } //child tables var rowsByPK = RestructureByPrimaryKey(datondef.MainTableDef, loadResult.RowsByParentKey); await LoadChildTablesRecursive(rowsByPK, db, dbdef, datondef.MainTableDef); daton.Key = key; daton.Recompute(datondef); return(new LoadResult { Daton = daton }); }
public PersistonDiff(DatonDef datondef, DatonKey key, string version) { DatonDef = datondef; Key = key; BasedOnVersion = version; }