Einen eigenen Connection-Pool implementieren 
From Theobald Software
In bestimmten Anwendungsszenarien kann es sinnvoll sein, einen so genannten Connection-Pool zu benutzen. Das bedeutet, dass sich mehrere Prozesse oder Threads einen Satz an SAP-Verbindungen teilen. Das kann zum Beispiel bei einer Web-Anwendung sinnvoll sein: 30 Benutzer arbeiten mit der Anwendung, aber nur 10 gleichzeitige Verbindungen zum SAP gibt es. Jedes mal, wenn ein Anwendungsprozess eine SAP-Verbindung braucht, wird eine aus dem Pool genommen, für die Dauer der Benutzung für andere gesperrt und nach Gebrauch wieder freigegeben.
Vorarbeit
Bevor wir richtig loslegen, benötigen wir eine neue Klasse namens R3ConnectionEx, die die bestehende ERPConnect-Klasse R3Connection erweitert.
Die beiden neuen Parameter LastUsage und IsInUse werden später benötigt.
class R3ConnectionEx : ERPConnect.R3Connection { private DateTime _LastUsage = DateTime.Now; public DateTime LastUsage { get { return _LastUsage; } set { _LastUsage = value; } } private bool _IsInUse = false; public bool IsInUse { get { return _IsInUse; } set { _IsInUse = value; } } }
Die Klasse ConnectionPool
Im folgenden werden die einzelnen Methoden der neuen Connection-Pool-Klasse erklärt.
Im Konstruktor wird ein Timer initialisiert, der später dafür sorgt, dass nicht benötigte Verbindungen nach einer gewissen Zeit auch wieder beendet werden.
class R3ConnectionPool { private System.Timers.Timer MyTimer = new System.Timers.Timer(); public R3ConnectionPool() { // When this static class is created // initialize the timer and handle elapsed event MyTimer.Interval = 1000; MyTimer.Elapsed += new System.Timers.ElapsedEventHandler(MyTimer_Elapsed); MyTimer.Enabled = true; } private Int32 _MaxNoOfConnection = 10; public Int32 MaxNoOfConnection { get { return _MaxNoOfConnection; } set { _MaxNoOfConnection = value; } } private string _ConnectionString = ""; public string ConnectionString { set { _ConnectionString = value; } } public Int32 CurrentNumberOfConnection { get { return MyConnectionList.Count; } }
In der generischen Liste MyConnectionList werden alle aktuelle aktiven Verbindungen gespeichert.
Wenn die letzte Nutzung 60 Sekunden her ist, und die Verbindung gerade nicht aktiv ist, wird sie beendet.
private System.Collections.Generic.List<R3ConnectionEx> MyConnectionList = new System.Collections.Generic.List<R3ConnectionEx>(); private void MyTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { // Loop through the list // and check, if a connection is not used for more than 60 second, // if so, close it an remove it from the list lock (MyConnectionList) { foreach (R3ConnectionEx con in MyConnectionList) { if (!con.IsInUse && con.LastUsage.AddSeconds(60) < DateTime.Now) { con.Close(); MyConnectionList.Remove(con); return; } } } }
Die beiden privaten, internen Funktionen AllocConnection() und FreeConnection() belegen eine Verbindung oder geben sie frei. Falls es keine freie Verbindung gibt, wird sie angelegt und zur Liste aktiver Verbindungen ergänzt.
private R3ConnectionEx AllocConnection() { lock (MyConnectionList) { foreach (R3ConnectionEx con in MyConnectionList) { if (!con.IsInUse) { con.IsInUse = true; return con; } } if (MyConnectionList.Count < this.MaxNoOfConnection) { R3ConnectionEx con = new R3ConnectionEx(); con.Open(this._ConnectionString); this.MyConnectionList.Add(con); con.IsInUse = true; return con; } if (MyConnectionList.Count >= this.MaxNoOfConnection) throw new Exception("Maximun Number of connection exceeded"); else throw new Exception("Unable to allocate a new connection"); } } private void FreeConnection(R3ConnectionEx con) { con.LastUsage = DateTime.Now; con.IsInUse = false; }
Unter normalen Umständen würde man die CreateFunction()-Methode direkt mit einer Verbindung aufrufen (z.B. Connection.CreateFunction()). Beim Pool wird das von der Pool-Klasse erledigt, die zunächst dynamisch eine freie Verbindung sucht.
Das RFCFunction-Object wird mit Hilfe der XML-Serialisierung und Deserialiserung gecashed, um nicht bei jedem CreateFunction()-Aufruf die Metadaten beim SAP abzurufen.
private Hashtable FunctionHash = new Hashtable(); public RFCFunction CreateFunction(string FunctionName) { lock (FunctionHash) { string xml = (string)FunctionHash[FunctionName]; if (xml == null) { // The function has not been created yet in this pool R3ConnectionEx con = this.AllocConnection(); try { RFCFunction func = con.CreateFunction(FunctionName); FreeConnection(con); // store in hash for later use FunctionHash.Add(FunctionName, func.SaveToXML()); return func; } catch (Exception e1) { // Check if connection is still alive // if not, remove it if (!con.Ping()) MyConnectionList.Remove(con); else FreeConnection(con); // rethrow exception throw e1; } } else { // We can create the function object without calling the CreateFunction method RFCFunction func = new RFCFunction(FunctionName); func.LoadFromXMLString(xml); return func; } } }
Analog dazu funktioniert das Ausführen der Funktion mit einer Pool-Verbindung:
public void ExecuteFunction(RFCFunction func) { R3ConnectionEx con = this.AllocConnection(); try { func.Connection = (R3Connection)con; func.Execute(); } catch (Exception e1) { // Check if connection is still alive // if not, remove it if (!con.Ping()) MyConnectionList.Remove(con); FreeConnection(con); // rethrow exception throw e1; } FreeConnection(con); }
Anwendung des Connection-Pools
Folgendes Konsolen-Programm zeigt die Anwendung des Connection-Pools.
Es werden zuerst 3 separate Threads gestartet und nachdem ein Benutzer auf eine Taste drückt, werden 3 weitere gestartet. Der Timer zeigt jede Sekunde, wieviele Verbindungen aktiv sind.
Je nach, wieviele Threads bereits beendet sind, wenn die nächsten gestartet werden, werden Verbindungen receycelt oder neu eröffnet.
Der Output des Programms zeigt folgendes:
- 3 Thread werden gestartet
- 2 davon werden beendet
- 3 neue werden gestartet
- korrektes Ergebnis: 4 SAP-Verbindungen, da nach den ersten 3 nur noch eine zusätzliche benötigt wurde.
Abschließend hier noch der Code des Pool-Demo-Programms:
class Program { static R3ConnectionPool ConPool = new R3ConnectionPool(); static System.Timers.Timer timer = new System.Timers.Timer(); [STAThread] static void Main(string[] args) { timer.Interval = 1500; timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); timer.Enabled = true; ConPool.ConnectionString = "USER=Theobald LANG=DE CLIENT=XXX SYSNR=XX ASHOST=XXX PASSWD=XXX "; Start3Threads("TH*","H*","X*"); Console.WriteLine("3 threads started. Press enter to start 3 more threads"); Console.Read(); Start3Threads("A*", "B*", "C*"); Console.WriteLine("3 addtional threads stated. Press enter to quit."); Console.ReadLine(); Console.ReadLine(); Console.ReadLine(); Console.ReadLine(); } static void Start3Threads(string SearchKey1, string SearchKey2, string SearchKey3) { System.Threading.Thread t4 = new System.Threading.Thread( new System.Threading.ParameterizedThreadStart(ExecuteALongRunningFunctionModule)); t4.Name = SearchKey1; t4.Start(SearchKey1); System.Threading.Thread t5 = new System.Threading.Thread( new System.Threading.ParameterizedThreadStart(ExecuteALongRunningFunctionModule)); t5.Name = SearchKey2; t5.Start(SearchKey2); System.Threading.Thread t6 = new System.Threading.Thread( new System.Threading.ParameterizedThreadStart(ExecuteALongRunningFunctionModule)); t6.Name = SearchKey3; t6.Start(SearchKey3); } static void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { Console.WriteLine("Current Number of connections: " + ConPool.CurrentNumberOfConnection); } static void ExecuteALongRunningFunctionModule(object SearchTerm) { RFCFunction func = ConPool.CreateFunction("BAPI_EMPLOYEE_GETLIST"); func.Exports["SUR_NAME_SEARK"].ParamValue = SearchTerm.ToString(); func.Exports["SEARCH_DATE"].ParamValue = "20070101"; ConPool.ExecuteFunction(func); Console.WriteLine(func.Tables["EMPLOYEE_LIST"].Rows.Count.ToString() + " rows received -> SearchKey: " + System.Threading.Thread.CurrentThread.Name); } }



