Einen eigenen Connection-Pool implementieren Deutsch

From Theobald Software

Jump to: navigation, search

English Version: How to implement a connection pool


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.


Download R3ConnectionPool.cs

Download R3ConnectionPool.vb


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.


Image:ThreadPoolDemo.png


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);
 
    }
 
}


DeutschEnglish
Personal tools
XtractQL Provider
Xtract RS