Problem Formulation
Pretend there is a fictional money pot existing unguarded on a computer server. The leprechauns have left their pot of gold unguarded and anyone can take $ money from the pot. But there is a catch: the clever elves have limited the amount of cash that can be taken per transaction request and guarded the resources to prevent multiple money grabs at the same time (resource thread locking). In a race to get all the money available from the money pot, several computer fairies are hacking into the money pot and competing against each other to get the most money. Each fairy starts their computer โclientโ to connect to the leprechaunsโ money pot server to drain as much as they can. To their disappointment, the computer fairies find that the more client connections (competition from other hackers) and requests they make to the money pot, the less $ money they receive compared to what they would receive if only one fairy hacker was hacking the money pot.
About
This project presents a simple, but fun โMoney Potโ Service and Client Application demonstration. The โMoney Potโ is a self-hosted (service host) WCF application with a GUI user interface to quickly demo and test the service with a client (both simple Windows Form Applications).
The service interface is defined not in the service application but in a Shared Library. This library defines the interface contracts for the $ money withdrawals and is referenced by both the client and service host projects.
A client โtesterโ windows form application tests the service and provides output to the user in a simple GUI.
In addition, a short discussion of concurrency to protect resources against multiple threads is shown along with charts, pictures, and test data to show that with multiple threads (clients) wanting the โmoney potโ resource, can diminish what resources an individual client thread can receive if they compete against one another.
Introduction & Terminology
Service Behaviors
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one “single” instance. In the โmoney potโ service behavior design there were two options studied:
Single: One Singleton instance is created for the server and all client calls.
PerCall: Each client call spins up a new service instance.
You can learn more about WCF Service Behaviors [here].
Service Data
Static: When the service spins up on the host it creates an instance on the server. This is true for either the Single or PerCall behavior. The static data is accessible on the class level instance. Any method calls or changes to static class data will be seen by all clients.
Instance: ย When the service spins up on the host it creates an instance on the server. This is true for either the Single or PerCall behavior. However, in the Single service behavior the static class and instance are shared by all clients. In the PerCall service behavior, the clients have their own private instance data that is inaccessible by other clients. The PerCall clients may still have access to modify the same static class methods and data properties โsharedโ by all clients just like the Single service behavior.
The Money Pots
There are two money pots defined in the service: static and instance.
Static Money Pot: The static money pot is one pot of $ money that will decrease over time as clients call the service and ask for money from the static money pot. This decrease in resource money will occur in either the Single or PerCall service behaviors.
Instance Money Pot: The instance money pot is also one pot of $ money resource that will behave differently depending on the service behavior configuration. If the service behavior is Single, then the instance pot of money will decrease over time with the multiple client calls and it will behave as though the clients are pulling $ money resources from a global or static money pot. However, if the service behavior is configured to PerCall, then the instance money pot resource is private and accessible only to that client who called it. If there is $1000 in the instance money pot, the client who called the instance money would have access to the entire pot. Furthermore, each time they called the service (new connection), they would see the full money resource regenerated and available (another $1000 available). If a client called the service (PerCall) and got $900 out of the instance money pot, disconnected, then called the service again, it would have $1000 available in the instance money pot, not $100.
Results Summary
I ran my service host in Single mode testing the effect of multiple clients on their ability to withdraw money from the static money pot. My code and tests simulated a thread lock of 1 one second on the static money pot as each client accessed it, others would wait while the ThreadPool would be in charge of client thread access under the safety of the thread lock.
Below is a summary of the average amount of money received per client given the number of clients trying to simultaneously access the money pot for a test run-time duration of ten seconds.
As you can see, the more clients that accessed the service, decreased the average money pot $ resource received by the client due to the thread locks. Note: I manually โfiredโ the start button as fast as I could on the individual test clients and there was some slight delay to be understood in that click/starting event that puts the clients I started first as getting more resources than those clients started later after my click delay. This is why I compare the averages.
Next, the test duration was increased to 120 seconds and I computed the average $ money received by each client for the given test case.
Once again, the average resource $ money received by a client decreased as competition increased.
Inequality Ratios
The 10 second client test had an inequality ratio of 50/23 or 2.17 with 10 clients while the 120 second client test had an inequality ratio of 590/131 or 4.50 with the same number of clients running. This meant that the longer the time duration that the clients tried to access the static money pot resource (protected by a 1 second thread lock) the more unequal their money pot share (average) became.
Architecture
The demo project consists of these component topics:
- WCF Service (Self-Hosted) Application Project โMoneyServerHostโ
- MoneyPotService (Code that Implements the Service Interface)
- config (Configuration for Service Host)
- Reference to the Shared Class Library
- Main GUI (Windows Form Application)
- Form Code โ Processes GUI User Interface
- Shared Class Library Project โSharedLibโ
- IMoneyPotService (Interface for Service)
- Client โTester to Serviceโ Windows Form Application Project โClientโ
- Reference to the Shared Class Library
- Main Form GUI User Interface
- Form Code โ Processes GUI User Interface
Shared Class Library
A Class Library project (SharedLib) was added to my Visual Studio solution. The code is available on GitHub [here].
IMoneyPotService (Interface for Service)
The ServiceContract for the Money Pot Service allows for two possible options: 1.) get money from the static money pot or 2.) get money from the instance money pot. The OperationContract will return an amount of money (decimal) to the client requester who is required to send them their client identification (integer). The service sends a fixed amount of $ money to the client. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 |
[ServiceContract(SessionMode = SessionMode.Allowed)] public interface IMoneyPotService { // Static [OperationContract(IsOneWay = false, IsInitiating = true)] decimal GetMoneyStatic(int clientID); // Instance [OperationContract(IsOneWay = false, IsInitiating = true)] decimal GetMoneyInstance(int clientID); } // end of interface |
WCF Service Application
A WCF Service (Self-Hosted) Application project was added to my Visual Studio solution. The code is available on GitHub [here].
MoneyPotService (Code that Implements the Service Interface)
The service implementation code controls thread access (locks) to the money pot resources and returns a fixed amount of money with each request for access to the static money pot or the instance money pot. ย The service references a shared class library called โSharedLibโ that contains the interface definition for the service.ย The code is available on GitHub [here].
Fields
The fields section contains object locks for concurrency, constant limits on the money pot withdrawals, and the money pot static and instance values. The static windows form GUI object is also defined here but instantiated in the main entry.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Object Locks for Concurrency private static object m_StaticLock = new object(); private object m_InstanceLock = new object(); // Limits on the Client Withdrawls private const decimal staticLimit = 10M; private const decimal instanceLimit = 1M; // 1 million dollars in the Static Money Pot (Starting) private static decimal m_StaticMoneyPot = 100000M; // 1 thousand dollars in the Instance Money Pot (Starting) private decimal m_InstanceMoneyPot = 1000M; // Form Objects private static Main frmMain; |
Main Entry
The main program entry for the application is in the service implementation. It instantiates a new form window (GUI) that is accessible from the service and runs it.
1 2 3 4 5 6 7 8 9 10 |
/// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(frmMain = new Main()); } |
Service Implementation Methods
Both the service implementations for the static and instance money pot contracts follow a similar pattern: 1.) Update the service message log 2.) Lock access to the Money Pot resource with a thread lock object 2.) Simulate a one second delay (sleep) 3.) If there is money left in the pot, withdraw the limit amount from the resource, update the GUI, update the message log, and return the result to the caller or else, return the remainder of the money pot (which could be zero) and do the same process of updates.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
/// <summary> /// GetMoneyInstance /// Returns an amount of money from the instance resource /// money pot /// </summary> /// <param name="clientID">the client identifer (int)</param> /// <returns>an amount of money (decimal) from the resources</returns> public decimal GetMoneyInstance(int clientID) { // Update the Event Log GUI frmMain.UpdateEventList($"Instance -> Client {clientID} calling the Instance Money Pot"); // Locks for Concurrency of the Instance Money Resource lock (m_InstanceLock) { System.Threading.Thread.Sleep(1000); // If there is less money left in the pot than the withdrawl amount if (m_InstanceMoneyPot - instanceLimit < 0) { decimal remainder = m_InstanceMoneyPot; m_InstanceMoneyPot = 0M; // Update the Instance Money Pot GUI frmMain.UpdateInstanceMoneyPot(m_InstanceMoneyPot); // Update the Event Log GUI frmMain.UpdateEventList($"Instance -> Sending {remainder:C} to client {clientID}"); return remainder; } // Plenty of Money left! else { m_InstanceMoneyPot -= instanceLimit; // Update the Instance Money Pot GUI frmMain.UpdateInstanceMoneyPot(m_InstanceMoneyPot); // Update the Event Log GUI frmMain.UpdateEventList($"Instance -> Sending {instanceLimit:C} to client {clientID}"); return instanceLimit; } // end of else } // end of lock } // end of method /// <summary> /// GetMoneyStatic /// Returns an amount of money from the static resource /// Money Pot /// </summary> /// <param name="clientID">the client identifer (int)</param> /// <returns>an amount of money (decimal) from the resources</returns> public decimal GetMoneyStatic(int clientID) { // Update the Event Log GUI frmMain.UpdateEventList($"Static -> Client {clientID} calling the Money Pot"); // Locks for Concurrency of the Static Money Resource lock (m_StaticLock) { System.Threading.Thread.Sleep(1000); // If there is less money left in the pot than the withdrawl amount if(m_StaticMoneyPot - staticLimit < 0) { decimal remainder = m_StaticMoneyPot; m_StaticMoneyPot = 0M; // Update the Static Money Pot GUI frmMain.UpdateStaticMoneyPot(m_StaticMoneyPot); // Update the Event Log GUI frmMain.UpdateEventList($"Static -> Sending {remainder:C} to client {clientID}"); return remainder; } // Plenty of Money left! else { m_StaticMoneyPot -= staticLimit; // Update the Static Money Pot GUI frmMain.UpdateStaticMoneyPot(m_StaticMoneyPot); // Update the Event Log GUI frmMain.UpdateEventList($"Static -> Sending {staticLimit:C} to client {clientID}"); return staticLimit; } // end of else } // end of lock } // end of method |
Application Configuration App.Config
The application configuration simple sets up the service model configuration including the endpoint, interface contract name, basic http protocol, and the base address for this demo. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?xml version="1.0"?> <configuration> <system.serviceModel> <services> <service name="MoneyServerHost.MoneyPotService" behaviorConfiguration="serviceBehavior"> <endpoint address="MoneyPotService" contract="SharedLib.IMoneyPotService" binding="basicHttpBinding"/> <endpoint address="mex" contract="IMetadataExchange" binding="basicHttpBinding"/> <host> <baseAddresses> <add baseAddress="http://localhost:8000"/> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior name="serviceBehavior"> <serviceMetadata/> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/> </startup> </configuration> |
GUI (Windows Form Application)
The Money Pot Server GUI is a simple Windows Form application with the intent of displaying the real-time dashboard of its static and instance money pot values during the demo tests. The code is available on GitHub [here].
GUI (Windows Form) Code
Code works behind the scenes to support the Windows Form GUI from launch to stop. When the form loads, it instances and opens a new Service Host (type MoneyPotService). When the form is closed, this service host is closed and the resource properly disposed.
The remainder of the form code handles the update of the money pot labels and log message data on the form list box. It receives the values to update from the business layer โMoney Pot Serviceโ. ย The code is available on GitHub [here].
Client โTester to Serviceโ Windows Form Application
The client โtester to serviceโ is a simple windows form application project in the same solution that connects to the โMoney Pot Serviceโ by use of a ChannelFactory proxy to a specified service name that is defined in the client application configuration. The client program will use this proxy to test the OperationContract or methods available in the ServiceContract and return the results to the user on the form window.
The client application shares and references the โShared Libraryโ โ a class library project. It has access to the Money Pot service interface to know exactly how it can consume the service with the help of a ChannelFactory proxy (discussed in the next section).
Form Code Features
The code behind the form is available on GitHub [here].
Client ID Textbox
The user must enter a valid integer into the text box before the client program will connect to the service. It does not check the id against other clients (this is just a simple demo program).
Start Button
When the start button is clicked the program launches to the button clicked event that starts a sequence of process including a new thread separate from the UI.
- The process is to first to try and parse the client id from the textbox entry before going further. If it detects an invalid entry, it notifies the user with pop-up window and returns to the main GUI. If the data entry is valid (an integer) and parsed, then the program sets the client id field and continues.
- GUI is Updated: The start button is disabled and the cancel button is enabled. The run time countdown counter is set to show the user how many second the test will run (default 10 seconds) from the constant field value.
- Time Elapsed and Timer fields are reset/initialized for the test run time.
- A cancellation token source is initialized for the โCancelโ button or cancel event scenarios in the program.
- Task Parallel Library (TPL) is setup to create a new thread for processing the call to the service with instructions and settings for what to do when the task is complete or cancelled.
- Action is now passed to the CallService() method on a new Task (parallel)
- A ChannelFactory object of type IMoneyPotService is created and opened.
- A service proxy object is created from the ChannelFactory object
- The Timer is Started
- This time object calls the OnTimedEvent() method every 1 second. The event increments a time elapsed property. It then computes the run time remaining. If there is additional time remaining, it updates the UI Thread GUI (see notes on the cross-thread synchronization). If the time has run out: it stops the timer, disables it, and also updates the UI Thread GUI.
- The program runs while the timer is enabled
- At every iteration we will check to see if cancellation was requested and throw a new OperationCanceledException if so requested.
- The program tries to connect to both the static and instance money pots on the service and get money out. If successful, it updates how much money it has received in its total fields.
- After the money grab is successful, the client updates its UI GUI thread (see notes on the cross-thread synchronization)
- Exceptions and the cancellation requests are handled.
- The task is now complete or cancelled.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
private void btnStart_Click(object sender, EventArgs e) { // Try Parse the User Input int clientID = default(int); // Validate Client ID if (!int.TryParse(txtClientID.Text, out clientID)) { MessageBox.Show("Enter a Valid Client ID Number.", "Invalid Input", MessageBoxButtons.OK); return; } // Set the Client ID ClientID = clientID; // Update the GUI btnStart.Enabled = false; btnCancel.Enabled = true; lblCountDown.Text = $"{runTime} seconds left"; // Reset Time Elapsed TimeElapsedSeconds = 0; // Enable the Timer myTimer.Enabled = true; // set the CancelationTokenSource object to a new instance m_TokenSource = new CancellationTokenSource(); // When Tasks complete, TPL provides a mechanism to automatically // continue execution on a WinForm or WPF UI thread.To do this // we need a handle to the UI thread which weโll use later TaskScheduler ui = TaskScheduler.FromCurrentSynchronizationContext(); // start the Task, passing it the name of the method that // will be the entry point for the Task and the token used for cancellation: Task myServiceCallTask = Task.Factory.StartNew(CallService, m_TokenSource.Token); // first continuation call will execute only when the Task completes successfully // notifies the user by showing a message box then resetting the buttons to their // default configuration. Notice that the last parameter to ContinueWith is โuiโ. // This tells ContinueWith to execute the lambda statements to execute within // the context of the UI thread.No Invoke / BeginInvoke needed here. Task resultOK = myServiceCallTask.ContinueWith(resultTask => { //MessageBox.Show("Completed Service Call Test", "Task Complete", MessageBoxButtons.OK, MessageBoxIcon.Information); btnStart.Enabled = true; btnCancel.Enabled = false; }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, ui); // second continuation call only executes if the task is cancelled Task resultCancel = myServiceCallTask.ContinueWith(resultTask => { //MessageBox.Show("Service Call Stopped by User", "Task Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Information); btnStart.Enabled = true; btnCancel.Enabled = false; lblCountDown.Text = "Cancelled"; // Send a Message //MessageBox.Show("Cancelled by User", "Cancelled", MessageBoxButtons.OK); }, CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled, ui); } // end of method |
Cancel Button
If the user manually cancels the task or the program has a scenario to cancel, it will call the method for the cancel button click event. This then cancels then formally cancels the token object that manages cancellation, stops and disables the timer, and updates the form UI Thread GUI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private void btnCancel_Click(object sender, EventArgs e) { // if a valid token source is available then signal // to any listeners that the Task has been cancelled if (m_TokenSource != null) { // Send Cancel to Token m_TokenSource.Cancel(); // Stop Timer myTimer.Stop(); myTimer.Enabled = false; // Update the UI Thread GUI UpdateLabelText(lblCountDown, $"Cancelled."); } // end of if } // end of method |
Labels and UI Thread Synchronization
When the client program runs in a test, it continuously updates the money pot totals and the test time remaining. These are shown as labels on the client GUI windows form.
Since the client work is done primarily on new thread that runs in parallel, anytime the GUI needs to be updated, there must be cross-thread synchronization. The static and instance money pot values are shown as labels on the GUI with the test time countdown remaining. The service call work needs to update these three labels and does so in a special method called UpdateLabelText(). The method receives two input parameters: Label control object and a message (string) to replace the label.
First the method checks if cross-thread synchronization is required by checking the specific label controlโs InvokeRequired property. So, if this method is called from our task, the test for InvokeRequired required will return true. This will then asynchronously call this method recursively. This time, however, since it was invoked upon the UI thread, InvokeRequired will be false so the else portion of the function executes, updating the Label.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/// <summary> /// Update a Text Label on GUI /// Update a value on the UI thread from within our task /// </summary> /// <param name="label">the label control (Label) to update</param> /// <param name="message">the message (string) to be changed on the label text</param> private void UpdateLabelText(Label label, string message) { // check if cross-thread synchronization is required // check the specific controlโs InvokeRequired property // So, if this method is called from our task, the // test for InvokeRequired required will return // true. This will then asynchronously call this method // recursively. This time, however, since it was invoked upon // the UI thread, InvokeRequired will be false so the else // portion of the function executes, updating the Label. if (label.InvokeRequired) { // If this condition is true, then we need to recursively // invoke this method on the UI thread label.BeginInvoke(new Action<Label, string>(UpdateLabelText), label, message); } // end of if // simply update the text box with the value given in the method else { label.Text = message; } // end of else } // end of method |
ChannelFactory Proxy and Application Configuration
The application configuration file (App.config) specifies the service endpoint address, binding, and contract information for the client to create a proxy and connect. The service contract information is in the Shared Class Library that has the service interface. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0"?> <configuration> <system.serviceModel> <client> <endpoint address="http://localhost:8000/MoneyPotService" binding="basicHttpBinding" contract="SharedLib.IMoneyPotService" name="BasicHttpBinding_IMoneyPotService"/> </client> </system.serviceModel> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/> </startup> </configuration> |
Demo
Video of 10 Second Test Running
Pictures of Tests After Completion
Code
The entire project code repository is available on GitHub [here].