I have a Lambda function in AWS (.NET Core) created to sweep Parameter Store parameters from Simple Systems Management into Memcached for easy / fast access by my serverless application. Despite my configuration function using the 'using' syntax for the Memcached client (which I expected to mean the connection would be released from the cache once the work was done) the connection count is continually climbing. My function is triggered once every 60 seconds to sync the parameter values.
Here is what the connection count looks like.
Here is my prototype function code:
public async Task<bool> LcsConfigurationSweeper(ILambdaContext context)
{
try
{
context.Logger.LogLine($"Configuration Sweeper activated");
ElastiCacheClusterConfig config = new ElastiCacheClusterConfig("*******", *******);
using (var client = new MemcachedClient(config))
{
GetParametersByPathRequest getParametersByPathRequest = new GetParametersByPathRequest
{
Recursive = true,
WithDecryption = true,
Path = "***"
};
var getParametersByPathResponse = await _systemsManagementClient.GetParametersByPathAsync(getParametersByPathRequest);
foreach (var parameter in getParametersByPathResponse.Parameters)
{
Object value; // Memcached value
if (client.TryGet(parameter.Name, out value))
{
if (value.ToString() != parameter.Value)
{
context.Logger.LogLine($"Configuration sweeper has detected {parameter.Name} has changed. Updating.");
if (client.Store(Enyim.Caching.Memcached.StoreMode.Replace, parameter.Name, parameter.Value))
{
context.Logger.LogLine($"Parameter {parameter.Name} updated in cache OK.");
return true;
}
else
{
context.Logger.LogLine($"Exception: Parameter {parameter.Name} unable to be updated in cache.");
}
}
}
else
{
if (client.Store(Enyim.Caching.Memcached.StoreMode.Set, parameter.Name, parameter.Value))
{
context.Logger.LogLine($"Parameter {parameter.Name} stored in cache OK.");
return true;
}
else
{
context.Logger.LogLine($"Exception: Parameter {parameter.Name} unable to be stored in cache.");
}
}
}
}
}
catch (Exception ex)
{
context.Logger.LogLine($"Exception: {ex.ToString()}");
}
return false;
}
Related
the following Redis Connection Exception occurs from the GetSentinelMasterConnection function when we use Redis.
the exception
(Sentinel: Failed connecting to configured master for service: my master)
and
(Sentinel: The ConnectionMultiplexer is not a Sentinel connection
)
the code of GetSentinelMasterConnection function :
public ConnectionMultiplexer
GetSentinelMasterConnection(ConfigurationOptions config,
TextWriter
log = null)
{
if (ServerSelectionStrategy.ServerType !=
ServerType.Sentinel)
throw new RedisConnectionException(ConnectionFailureType.UnableToConnect,
"Sentinel: The ConnectionMultiplexer is not a Sentinel connection. Detected as: " + ServerSelectionStrategy.ServerType);
if (string.IsNullOrEmpty(config.ServiceName))
throw new ArgumentException("A ServiceName must be specified.");
lock (sentinelConnectionChildren)
{
if (sentinelConnectionChildren.TryGetValue(config.ServiceName, out var sentinelConnectionChild) && !sentinelConnectionChild.IsDisposed)
return sentinelConnectionChild;
}
bool success = false;
ConnectionMultiplexer connection = null;
var sw = Stopwatch.StartNew();
do
{
// Get an initial endpoint - try twice
EndPoint newMasterEndPoint = GetConfiguredMasterForService(config.ServiceName)
?? GetConfiguredMasterForService(config.ServiceName);
if (newMasterEndPoint == null)
{
throw new RedisConnectionException(ConnectionFailureType.UnableToConnect,
$"Sentinel: Failed connecting to configured master for service: {config.ServiceName}");
}
EndPoint[] replicaEndPoints = GetReplicasForService(config.ServiceName)
?? GetReplicasForService(config.ServiceName);
// Replace the master endpoint, if we found another one
// If not, assume the last state is the best we have and minimize the race
if (config.EndPoints.Count == 1)
{
config.EndPoints[0] = newMasterEndPoint;
}
else
{
config.EndPoints.Clear();
config.EndPoints.TryAdd(newMasterEndPoint);
}
foreach (var replicaEndPoint in replicaEndPoints)
{
config.EndPoints.TryAdd(replicaEndPoint);
}
connection = ConnectImpl(config, log);
// verify role is master according to:
// https://redis.io/topics/sentinel-clients
if (connection.GetServer(newMasterEndPoint)?.Role().Value == RedisLiterals.master)
{
success = true;
break;
}
Thread.Sleep(100);
} while (sw.ElapsedMilliseconds < config.ConnectTimeout);
if (!success)
{
throw new RedisConnectionException(ConnectionFailureType.UnableToConnect,
$"Sentinel: Failed connecting to configured master for service: {config.ServiceName}");
}
// Attach to reconnect event to ensure proper connection to the new master
connection.ConnectionRestored += OnManagedConnectionRestored;
// If we lost the connection, run a switch to a least try and get updated info about the master
connection.ConnectionFailed += OnManagedConnectionFailed;
lock (sentinelConnectionChildren)
{
sentinelConnectionChildren[connection.RawConfig.ServiceName] = connection;
}
// Perform the initial switchover
SwitchMaster(RawConfig.EndPoints[0], connection, log);
return connection;
}
I just got done adding Xbox support code to my project, and have run into at least two issues.
The first involves save data sync which is working just fine, however when the game reads the user's login data on Windows it behaves as if login has not been completed - no gamertag is displayed in the corner, and the login provider throws error 0x87DD0005 regardless of the number of retry attempts.
Execution of the code is just fine on Xbox - only Windows seems to be affected by this. I'm also targeting the creator's showcase initially (or at least until I can get to where I'm ready for another run at ID#Xbox) so achievements and the like aren't a concern right now.
The following is the code I'm using (and in no particular order):
public void doStartup()
{
getData(-1);
for (int i = 0; i <= 5; i++)
{
getData(i);
}
ContentViewport.Source = new Uri("ms-appx-web:///logo.html");
}
public async void getData(int savefileId)
{
var users = await Windows.System.User.FindAllAsync();
string c_saveBlobName = "Advent";
//string c_saveContainerDisplayName = "GameSave";
string c_saveContainerName = "file" + savefileId;
if (savefileId == -1) c_saveContainerName = "config";
if (savefileId == 0) c_saveContainerName = "global";
GameSaveProvider gameSaveProvider;
GameSaveProviderGetResult gameSaveTask = await GameSaveProvider.GetForUserAsync(users[0], "00000000-0000-0000-0000-00006d0be05f");
//Parameters
//Windows.System.User user
//string SCID
if (gameSaveTask.Status == GameSaveErrorStatus.Ok)
{
gameSaveProvider = gameSaveTask.Value;
}
else
{
return;
//throw new Exception("Game Save Provider Initialization failed");;
}
//Now you have a GameSaveProvider
//Next you need to call CreateContainer to get a GameSaveContainer
GameSaveContainer gameSaveContainer = gameSaveProvider.CreateContainer(c_saveContainerName);
//Parameter
//string name (name of the GameSaveContainer Created)
//form an array of strings containing the blob names you would like to read.
string[] blobsToRead = new string[] { c_saveBlobName };
// GetAsync allocates a new Dictionary to hold the retrieved data. You can also use ReadAsync
// to provide your own preallocated Dictionary.
GameSaveBlobGetResult result = await gameSaveContainer.GetAsync(blobsToRead);
string loadedData = "";
//Check status to make sure data was read from the container
if (result.Status == GameSaveErrorStatus.Ok)
{
//prepare a buffer to receive blob
IBuffer loadedBuffer;
//retrieve the named blob from the GetAsync result, place it in loaded buffer.
result.Value.TryGetValue(c_saveBlobName, out loadedBuffer);
if (loadedBuffer == null)
{
//throw new Exception(String.Format("Didn't find expected blob \"{0}\" in the loaded data.", c_saveBlobName));
}
DataReader reader = DataReader.FromBuffer(loadedBuffer);
loadedData = reader.ReadString(loadedBuffer.Length);
if (savefileId == -1)
{
try
{
System.IO.File.WriteAllText(ApplicationData.Current.TemporaryFolder.Path + "\\config.json", loadedData);
}
catch { }
}
else if (savefileId == 0)
{
try
{
System.IO.File.WriteAllText(ApplicationData.Current.TemporaryFolder.Path + "\\global.json", loadedData);
}
catch { }
}
else
{
try
{
System.IO.File.WriteAllText(ApplicationData.Current.TemporaryFolder.Path + "\\file" + savefileId + ".json", loadedData);
}
catch { }
}
}
}
public async void InitializeXboxGamer()
{
try
{
XboxLiveUser user = new XboxLiveUser();
if (user.IsSignedIn == false)
{
SignInResult result = await user.SignInSilentlyAsync(Window.Current.Dispatcher);
if (result.Status == SignInStatus.UserInteractionRequired)
{
result = await user.SignInAsync(Window.Current.Dispatcher);
}
}
System.IO.File.WriteAllText(ApplicationData.Current.TemporaryFolder.Path + "\\curUser.txt", user.Gamertag);
doStartup();
}
catch (Exception ex)
{
// TODO: log an error here
}
}
I finally managed to figure out why the Xbox was working but Windows was not: it was a platform support issue. In the game's creator's dashboard for Xbox Live there's a settings window that allows the support of the game to be determined. Because I originally had separate builds for Xbox and Windows, only the Xbox support item was checked, so I went ahead and also checked off for Desktop support. After saving the changes, I resubmitted with the new configuration and now it works properly.
I have two reliable queues and they are being accessed by two guest executables and each of them access their own. Sometimes the function I use to access them doesn't update the reliable queue object in the function and the wrong request is sent to the wrong guest executable.
What happens is that the clientId is passed by the guest executable to this function in the Get request. Let us say that there are two clientId(s) called T1 and T2.
What happens is that the guest executable (client) T2 at times gets the request that was meant for T1. Even though I tried line by line debugging the parameters passed to this function are correct.
Here is my API's POST that is passed a json to be added to the queue for the clients to receive from the GET
[HttpPost("MarketInfo")]
public JObject GetMarketInfo([FromBody] JObject jObject)
{
List<JToken> clients = jObject.GetValue("clients").ToList();
string json;
JObject response = new JObject();
JArray jsonArray = new JArray();
try
{
foreach (JToken client in clients)
{
var id = Guid.NewGuid();
json = "{'name':'MarketInfo','id':'" + id.ToString() + "','mtClientId':'" + terminal["name"].ToString() + "','parameters':{'symbol':'" + terminal["symbol"].ToString() + "','property':24}}";
bool result = _requestsCollectionHandler.CreateRequestForClient(JObject.Parse(json));
JObject clientResponse = new JObject();
if (result==true)
{
clientResponse["name"] = client["name"].ToString();
clientResponse["guid"] = id.ToString();
jsonArray.Add(clientResponse);
}
else
{
clientResponse["name"] = terminal.Children()["name"].ToString();
clientResponse["guid"] = "ERROR";
jsonArray.Add(terminalResponse);
}
}
response["clients"] = jsonArray;
return response;
}
catch (Exception e)
{
Debug.Write(e.Message);
return null;
}
}
This is the json that we pass to this API
{"clients":[{"name":"T1","symbol":"SomeInfo"},{"name":"T2","symbol":"SomeInfo"}]}
The problem is always with the clients object that is passed first.
Before I explain further let me also share the code for the client's HttpGet
[HttpGet("{clientId}")]
public string Get([FromRoute] string clientId)
{
try
{
string request = _requestsCollectionHandler.GetRequestJsonFromQueue(clientId);
return request;
}
catch(Exception e)
{
return e.Message;
}
}
This is the function that creates an object that is to be added by another function in the reliable queue
public bool CreateRequestForClient(JObject jObject)
{
try
{
this._jObject = new JObject(jObject);
CreateKey();
AddToRequestToQueueAsync();
return true;
}
catch (Exception e)
{
Debug.Write(e.Message);
_exceptionMessage = e.Message;
return false;
}
}
private void CreateKey()
{
dynamic data = JObject.Parse(_jObject.ToString(Newtonsoft.Json.Formatting.None));
string name = data.name;
string id = data.id;
string clientId = data.clientId;
_key.id = id;
_key.name = name;
_key.clientId = clientId;
//key.timestamp = GetTimestamp();
_key.timestamp = GetTimestamp();
_key.requestJson = _jObject.ToString(Newtonsoft.Json.Formatting.None);
}
_key is a private variable in class a custom class
This is the function in my class of request handler that adds the requests to the queue
private void AddToRequestToQueueAsync()
{
var transaction = this._stateManager.CreateTransaction();
CancellationToken cancellationToken
= new CancellationToken(false);
try
{
string queue = _key.clientId;
IReliableConcurrentQueue<TerminalResponseKey> reliableQueue =
_stateManager.GetOrAddAsync<IReliableConcurrentQueue<TerminalResponseKey>>(queue).Result;
transaction = this._stateManager.CreateTransaction();
if (reliableQueue!=null)
{
long count = reliableQueue.Count;
reliableQueue.EnqueueAsync(transaction, _key);
count = reliableQueue.Count;
transaction.CommitAsync().Wait();
}
else
{
transaction.Abort();
}
}
catch
{
transaction.Abort();
throw;
}
}
This is function that is used by the client
public string GetRequestJsonFromQueue(string clientId)
{
string queue = clientId;
try
{
IReliableConcurrentQueue<TerminalResponseKey> reliableQueue =
this._stateManager.GetOrAddAsync<IReliableConcurrentQueue<TerminalResponseKey>>(queue).Result;
if(reliableQueue != null)
{
ConditionalValue<TerminalResponseKey> key =
reliableQueue.TryDequeueAsync(transaction).Result;
if(key.HasValue)
{
string request = key.Value.requestJson;
transaction.CommitAsync().Wait();
return request;
}
}
else
{
transaction.Abort();
}
return "NO QUEUE";
}
catch (Exception e)
{
Debug.WriteLine(e);
transaction.Abort();
return e.InnerException.Message;
}
}
As far as I have found out I think my problem is in this function above. Because I don't know how the client T2 or client T1 gets another client's queue because the parameters determining the queue are their IDs and are totally unique.
These Ids are also passed correctly to this:
IReliableConcurrentQueue<TerminalResponseKey> reliableQueue =
this._stateManager.GetOrAddAsync<IReliableConcurrentQueue<TerminalResponseKey>>(queue).Result;
As you can see that we have queue=clientId
I have tried adding proper timespans but it was of no use as there is no exception thrown for OperationTimedOut. Furthermore since I am new to ServiceFabric I maybe totally doing anything wrong.
PS: Sorry for maybe a lot of jumbled up and confused code and question AND SOME OF THE INFORMATION IS OBFUSCATED DUE TO CONFIDENTIALITY BUT NOTHING OBSTRUCTING THE UNDERSTANDING OF THIS IS HIDDEN (I Hope not an issue)
I hope this is not an issue maybe an error I am overlooking at my side
When you put the request in the queue, in AddToRequestToQueueAsync(), the name of the queue is set from _key.terminalId (and I don't see where you assign it), but when you read from it, in GetRequestJsonFromQueue(), the clientId
is used as the queue name.
I have a very ugly problem with a code made by a co worker. The action is within a TransactionScope. First a database insert is performed:
var billingRecord = new Billing
{
ACCOUNT_ID = AccountId,
AMOUNT = Amount,
};
_ObjectContext.AddToBilling(billingRecord);
_ObjectContext.SaveChanges(SaveOptions.None)
Then a web service call is performed:
var webServiceCallResult = Request(params);
if (webServiceCallResult.Result == 1)
{
success = true;
}
else
{
success = false;
}
If the web service call is ok, the transaction is completed in a finally block:
finally
{
if (success)
{
tran.Complete();
_ObjectContext.AcceptAllChanges();
_Logger.Info(String.Format("Transaction completed"));
}
else
{
_Logger.Info(String.Format("Transaction uncompleted"));
}
}
The problem is that for some reason, some records are not stored in the database. I tested a lot of transactions but that never happen to me in development environment, but sometimes happens in production environment. When I say "sometimes" it's because this situation is very unusual or rare.
Looking in the log file, I can see the message:
Transaction completed
and no exceptions displayed, so the web service call is good and the transaction was completed but the record was not inserted to table.
I know that is not necessary to create a TransactionScope because there is a only a insert and no additional database operations are needed. The object context of EF is created like a global var in the class and is never disposed , that is a bad practice but as far as I have knowledge the ObjectContext will be destroyed by the garbage collector, so I think that is not what causes the problem.
I read a lot about transactions and the correct way to use a Entity Framework ObjectContext and the methods SaveChanges() and AcceptAllChanges() and even the code is not using the best practices that should work. I don't want only refactor the code, I would like to know the real reason of the problem.
I would appreciate your help.
I am using:
ASP.NET MVC 3,
Entity Framework 5.0,
Ninject,
DevExpress
Here is the the complete class:
public class Implementation : IExecute
{
private readonly Logger _Logger;
private readonly ExampleEntities _ObjectContext = new ExampleEntities();
public TopUpExecuteImplementation()
{
_Logger = LogManager.GetLogger("Logger");
}
public Response perfomOperation(String account, String amount)
{
var success = false;
using (var tran = new System.Transactions.TransactionScope())
{
try
{
var accountRecord =
_ObjectContext.Accounts.First(
p => p.Account.Equals(account, StringComparison.InvariantCultureIgnoreCase));
var billingRecord = new Billing
{
ACCOUNT = account,
AMOUNT = amount,
};
_ObjectContext.AddToBillings(billingRecord);
_ObjectContext.SaveChanges(SaveOptions.None);
var webServiceCallResult = Request(account,amount);
_Logger.Info(String.Format("Request Result {0} ", webServiceCallResult.Result));
if (webServiceCallResult.Result == 0)
{
success = false;
}
else
{
if ((String.IsNullOrEmpty(webServiceCallResult.statusCode) == false) &&
(webServiceCallResult.statusCode.Equals("Success",
StringComparison.InvariantCultureIgnoreCase)))
{
success = true;
}
else
{
success = false;
}
}
}
catch (OptimisticConcurrencyException ex)
{
_Logger.Info(String.Format("Exception type {0} Exception {1} Inner Exception {2} ",
ex.GetType().ToString(), ex.Message,
ex.InnerException != null ? ex.InnerException.Message : String.Empty));
_ObjectContext.SaveChanges();
success = true;
}
catch (Exception e)
{
_Logger.Info(String.Format("Exception type {0} Exception {1} Inner Exception {2} ",
e.GetType().ToString(), e.Message,
e.InnerException != null ? e.InnerException.Message : String.Empty));
success = false;
}
finally
{
if (success)
{
tran.Complete();
_ObjectContext.AcceptAllChanges();
_Logger.Info(String.Format("Transaction completed"));
}
else
_Logger.Info(String.Format("Transaction uncompleted"));
}
}
return returnValue;
}
}
I have two wcf services Client.svc and Admin.svc.
A method in client calls admin service method in every 5 seconds. And Admin's method which is been called by client verifies that if this method is not called within 5 seconds then update database with status "NotResponding" else update it with "IsAlive" value.
All this should be done on a separate thread.
I have written some code wherein Client uses Timer to call that method in every 5 seconds.
public static void RegisterHeartBeat(PingRequest pingRequest)
{
try
{
string heartBeatInterval = Phoenix.API.Utility.ConfigReader.GetAppSettingsValue("HeartBeatInterval");
int timeInSeconds = -1;
Int32.TryParse(heartBeatInterval, out timeInSeconds);
if (timeInSeconds != -1)
{
TimerCallback timerCallHeartBeat = new TimerCallback(CallRegisterHeartBeat);
Timer timer = new Timer(timerCallHeartBeat, pingRequest, 0, (timeInSeconds * 1000)); //Multiplying by 1000, converts seconds to milliseconds
}
else
{
Exception ex = new Exception("HeartBeatInterval is not configured in web.config file");
Phoenix.Client.API.BLL.Common.CommonUtility.CreateResultAndLogClientException(null, null, ex);
}
}
catch (Exception ex)
{
Phoenix.Client.API.BLL.Common.CommonUtility.CreateResultAndLogClientException(null, null, ex);
}
}
private static void CallRegisterHeartBeat(object state)
{
PhoenixClientBLL.Admin.InternalClient internalClient = new PhoenixClientBLL.Admin.InternalClient("BasicHttpBinding_IInternal");
if (state != null)
{
//AdminAPI accepts Admin.PingRequest parameter which has a different format than ClientAPI PingRequest.
//Thus, a new object of admin ping request type is created.
Phoenix.API.ClientServiceContracts.DataContracts.PingRequest pingRequestDC = state as Phoenix.API.ClientServiceContracts.DataContracts.PingRequest;
//AdminAPI
PhoenixClientBLL.Admin.PingRequest pingRequest = new PhoenixClientBLL.Admin.PingRequest();
//Test Agent ID
pingRequest.TestAgentId = Guid.Parse(pingRequestDC.TestAgentId);
//Test Agent Status is not set because it will be decided in ADMIN API as per the interval difference.
internalClient.RegisterHeartBeat(pingRequest);
}
}
In Admin, I check the last update date and the current date with the difference of time to update database accordingly.
public static void RegisterHeartBeat(PingRequest pingRequest)
{
int status = 0;
DateTime startTime, endTime;
int testAgentId = -1;
string heartBeatIntervalValue = Phoenix.API.Utility.ConfigReader.GetAppSettingsValue("HeartBeatInterval");
int heartBeatInterval = -1;
if(String.IsNullOrEmpty(heartBeatIntervalValue))
{
Common.CommonUtility.CreateResultAndLogException(null, null, new Exception("HeartBeatInterval is not configured in the configuration file"));
}
else
{
try
{
string key = pingRequest.TestAgentId.ToString();
if (!String.IsNullOrEmpty(key))
{
if (!heartBeatTimeStamp.ContainsKey(key))
{
heartBeatTimeStamp.Add(key, System.DateTime.Now);
}
else
{
endTime = DateTime.Now;
if (heartBeatTimeStamp[key].HasValue)
{
startTime = heartBeatTimeStamp[key].Value;
var timeDiff = new TimeSpan(endTime.Ticks - startTime.Ticks);
//Check the configured heart beat interval value
Int32.TryParse(heartBeatIntervalValue, out heartBeatInterval);
if (heartBeatInterval != -1)
{
if (timeDiff.Seconds > heartBeatInterval)
{
// add update NotResponding = 3 ..
Int32.TryParse(pingRequest.TestAgentId.ToString(), out testAgentId);
//If Test Agent ID is converted into integer than update table else log the error.
if (testAgentId != -1)
{
status = DAO.TestAgentDAO.RegisterHeartBeat(testAgentId, (int)TestAgentStatus.NotResponding);
}
else
{
Common.CommonUtility.CreateResultAndLogException(null, null, new Exception("Cannot convert Test Agent ID Data type from GUID to Integer"));
}
//Sql Error
if (0 != status)
{
Common.CommonUtility.CreateResultAndLogSqlError(null, status, null);
}
}
else
{
// add update IsAlive= 4
Int32.TryParse(pingRequest.TestAgentId.ToString(), out testAgentId);
//If Test Agent ID is converted into integer than update table else log the error.
if (testAgentId != -1)
{
status = DAO.TestAgentDAO.RegisterHeartBeat(testAgentId, (int)TestAgentStatus.IsAlive);
}
else
{
Common.CommonUtility.CreateResultAndLogException(null, null, new Exception("Cannot convert Test Agent ID Data type from GUID to Integer"));
}
//Sql Error
if (0 != status)
{
Common.CommonUtility.CreateResultAndLogSqlError(null, status, null);
}
}
}
else
{
Common.CommonUtility.CreateResultAndLogException(null, null, new Exception("Invalid HeartBeatInterval Value"));
}
}
}
}
else
{
Common.CommonUtility.CreateResultAndLogException(null, null, new Exception("Test Agent ID is incorrect or does not exists"));
}
}
catch (Exception ex)
{
Common.CommonUtility.CreateResultAndLogException(null, null, ex);
}
}
}
But my timer behaves in a wierd manner and never calls the admin method..
Can you please check it why? or any other logic needs to be implemented here.
Thanks
Priyanka
You can probably use a scheduler instead of a timer. There is an open source scheduler Quartz .Net available for .NET. This can trigger your calls every 5 seconds.
In your static main class instantiate a timer and create a elapsed event handler for the timer to go to when the 5 seconds are up.
in the elapsed event handler, call your method you want to run every 5 seconds. Keep in mind the timer and event handler span threads so you need to realize you could have two events happening at the same time - meaning code in a thead safe maner...
Example of timer
http://msdn.microsoft.com/en-us/library/system.timers.timer(v=VS.71).aspx
i just reread your post, you are using a timer... Keep in mind THREADSAFE. if you need to stop your timer before going into the method you need called when the elapsed even ocurs.
Are you starting the Timer anywhere? Or setting Enabled to true?
http://msdn.microsoft.com/en-us/library/system.timers.timer.start.aspx