Schrift
[thread]12474[/thread]

Vorgehensweise bei der Fehlersuche (Objekt wird zu früh zerstört)



<< >> 8 Einträge, 1 Seite
xtomcatx
 2008-09-10 18:41
#114509 #114509
User since
2006-08-27
31 Artikel
BenutzerIn
[default_avatar]
Hallo zusammen,

vorweg: das Problem tritt bei der CGI-Programmierung auf, hat meiner Meinung nach aber nichts damit zu tun (siehe ganz unten), daher bitte ggf. verschieben, wenn das CGI-Forum der falsche Ort ist.

Bei der Fehlersuche eines Skripts habe ich festgestellt, dass ein Objekt zu früh zerstört wird. Die Frage ist, wieso und wie ich das sauber verhindern kann.
Konkret betroffen ist das Zusammenspiel zwischen Apache::Session und DBI. Apache::Session bietet zum einen die Möglichkeit, selbst eine Datenbankverbindung aufzumachen, zum anderen ein bereits bestehendes DBI-Handle zu verwenden. Im ersten Fall gibt es nie Probleme, im zweiten Fall manchmal.

Für alle, die Apache::Session noch nicht verwendet habe, kurzer Überblick über die Funktionsweise:
Man bindet einen Hash an die Klasse und initiiert auf diese Weise ein Objekt. Dieses Objekt bekommt eine _session_id und wird sofort in der DB abgelegt. Über den Hash kann man auf diese _session_id und beliebige weitere Schlüssel-Wert-Paare zugreifen. Diese weiteren Schlüssel-Wert-Paare werden aber erst dann in der Datenbank abgelegt, wenn der Destruktor aufgerufen wird. In manchen Situationen wird allerdings der Destruktor von DBI zuerst aufgerufen, das Update der Session-Tabelle erfolgt nicht mehr. Ich denke, dass das kein Fehler von Apache::Session ist, sondern entweder mein eigener oder ein Fehler im Garbage Collector.
Eine Programmversion bringt bei wiederholter Ausführung scheinbar immer das gleiche Ergebnis, es ist also nicht zufällig, was passiert.

Mir geht es darum, diesen Fehler zu finden, ich habe allerdings das Problem, dass ich bisher in keinem Minimalbeispiel diesen Fehler reproduzieren konnte, obwohl das Problemskript auch schon nicht übermäßig lange ist. Wenn ich etwas annehmbar kurzes gefunden habe, dann werde ich das noch nachreichen.

Die Situation ist ungefähr die:
- das Hauptprogramm main.pl
- ein Paket Model.pm, das über einen Konstruktor die Verbindung zur Datenbank herstellt, dann das Objekt $model zurückgibt über das man dann Abfragen vornimmt. Das DBI-Handle wird auch übergeben an:
- ein Paket Session.pm, das mit einem DBI-Objekt instantiiert wird und mit einer Methode create(), die Apache::Session verwendet, um Sessions abzulegen. Es wird eine Hashreferenz, die zum Session.pm-Objekt gehört an Apache::Session::MySQL gebunden.

Nicht gerade hilfreich bei der Fehlersuche ist, dass Änderungen, die scheinbar überhaupt nichts mit dem Sachverhalt zu tun haben, dennoch das Ergebnis ändern.
1. Ausgangslage: Alles funktioniert, das übergebene DBI-Handle wird korrekt verwendet, die Destruktoren arbeiten in der richtigen Reihenfolge. Passt.
2. Ich lege eine Methode Model::foobar an:
Code (perl): (dl )
sub foobar { my $self = shift; }

die Methode wird nicht aufgerufen, keine Änderung, alles passt.
3. Ich ändere die Methode Model::foobar ab:
Code (perl): (dl )
sub foobar { my $self = shift; my $param = shift; }

die Methode wird im ganzen Programm nicht einmal aufgerufen, der Methodenname ist absolut egal, ebenso die Variablenbezeichnungen: Das Programm funktioniert nicht mehr korrekt, der DBI-Destruktor wird vor dem Apache::Session-Destruktor aufgerufen.
4. Es gibt mehrere Möglichkeiten, das Programm wieder zum laufen zu bekommen, aber alle sind gebastelt. Eine der folgenden Möglichkeiten reicht aus, das Programm wieder funktionstüchtig zu machen:
4a. Nach Erzeugung des Models erstelle ich im Hauptprogramm eine zusätzliche Referenz auf das DBI-Objekt.
4b. Ich verzichte darauf, dem Session-Hash Ergebnisse aus einer Datenbankabfrage zuzuweisen.
4c. Ich ergänze use CGI::Carp qw(fatalsToBrowser);
4d. Ich wende das "tie" auf einen normalen Hash an und referenziere in meinem Session.pm-Objekt auf diesen.
4e. Um den Destruktor künstlich aufzurufen, ergänze ich am Ende des Hauptprogramms ein delete( $session->{data} ). (Das ist die Referenz auf den "ge_tie_den" Hash)
4n. to be continued.

Alles in allem sind ein paar nachvollziehbare Varianten darunter und ein paar weniger nachvollziehbare. Die Lösung 4e gefällt mir bisher am besten, aber ob das eine wirklich saubere Lösung ist, weiß ich nicht und auch nicht, ob das immer funktioniert.
Der Perl-GC ist als Referenz-zählende Implementierung ja nicht so wahnsinnig aufwendig gestaltet, gibt es deswegen vielleicht Probleme? Aber eigentlich halte ich dieses Problem für ein Standardproblem, hat eigentlich nichts mit CGI oder Apache::Session oder DBI zu tun. Daher zweifle ich eher an meinem Programmcode, der aber zu wenig komplex ist, um grundsätzliche Denkfehler zu vermuten.

Tjo, doch etwas länger geworden, schönen Dank fürs Lesen.
Viele Grüße
Martin

Edit: Wodurch könnte das Problem begründet sein und wo könnte ich ansetzen, um nicht nur die Symptome zu beheben (explizite DESTROY-Auslösung durch delete()-Aufruf), sondern die Ursache zu finden (möglicherweise ein Problem mit meinen Referenzen?)? Ist vielleicht irgendwo das Überschreiben/Ergänzen eines Destruktors notwendig? Meiner Meinung nach eher nicht, denn eine Referenz des Model-Objekts wird vom Session-Objekt verwendet und nicht umgekehrt. Ich kann das zerstören des Models also eigentlich nicht hinauszögern.
Nochwas: Zwischenzeitlich hatte ich in den verwendeten CPAN-Modulen Debug-Ausgaben eingebaut, die mir durch die Ausgabe auf STDERR angezeigt haben, in welcher Reihenfolge die Destruktoren sowie Folge-Methoden abgearbeitet werden. Dabei ist mir aufgefallen, dass im Fehlerfall teilweise Überschneidungen da waren, die Ausführung also sehr zeitnah gewesen sein muss.
Gast Gast
 2008-09-10 19:11
#114510 #114510
Mir scheint du durchbrichst dein Schnittstellenmodell. Wenn du ein Modul hast (aus dem du ein Objekt generierst), indem alle Datenbankabfragen enthalten sind, warum übergibst du noch das DBI-Objekt. Sinnvoller wäre es das Objekt zu übergeben, welches das DBI-Objekt enthält.
Eventuell könntest du die DBI-Klasse (Modul) überladen und um deine Funktionen ergänzen. (siehe "use base")

Ich sehe das eigendliche Problem im Zusammenspiel von persistenten Referenzen auf Objekte in Objekten (Apache::Session) und nicht persistenten. Es ist eine Vermutung, aber ich würde auf schlechte Programmierung in "Apache::Session" tippen.
xtomcatx
 2008-09-10 21:04
#114514 #114514
User since
2006-08-27
31 Artikel
BenutzerIn
[default_avatar]
Danke für die flotte Antwort. Mit dem Schnittstellenmodell hast Du recht, das hat mir von Anfang an auch etwas aufgestoßen, aber darüber, dafür ein eigenes Interface zu implementieren, hab ich ehrlich gesagt gar nicht nachgedacht. Werde ich jetzt mal tun, ist ne gute Idee und dürfte eigentlich kein allzu großer Aufwand sein. Vom Adapter mal abgesehen, müsste ich im Destruktor dann prüfen, ob noch ein Session-Objekt besteht und erst das beenden, oder? Eigentlich müsste ich das dann besser allgemein für alle Objekte machen. Naja, aa muss ich mir mal Gedanken drüber machen.

Darüber müsste das Problem zwar auch zu lösen sein, aber wie Du schon sagst, ist das nicht der Quell allen Übels. Kannst Du mir das mit den persistenten Referenzen genauer erklären? Eigentlich müsste der GC ja wissen, dass noch mindestens eine Refernz auf das Objekt besteht. Du meinst, dass A::S jetzt eine Referenz verwendet, die nur einen bestimmten Gültigkeitsbereich hat und diese zum Zeitpunkt des Zerstörung nicht mehr existiert, oder?
Aber müsste das Problem dann nicht auch in den funktionierenden Konstellationen auftreten?
Gast Gast
 2008-09-11 00:25
#114518 #114518
Mit den Persistenten Referenzen war ich auf dem Holzweg.

Ich habe mal etwas genauer in Apache::Session geschaut.

Ich gehe jetzt eher davon aus das einer der Destroy-Blöcke schuld ist. Wenn in wenigstens einem zu früh die Verbindung zur Datenbank geschlossen wird, können keinen Daten mehr gespeichert werden.

Ich habe bei übergeben DBI-Objekten mindestens 2 Referenzen gezählt, die parallel existieren und im anderen Fall unabhängig zu sein scheinen. In zwei DESTROY-Blöcken wird DBI::disconnect aufruft. ("Apache::Session::Lock::MySQL" "Apache::Session::Store::MySQL") Es kann sein, dass der Garbage Collector schon die Verbindung schließt bevor die Daten gesichert wurden. Aber genaueres weiß ich nicht. Die Struktur ist recht komplex und ich blicke da noch nicht so ganz durch, wie der Ablauf ist. Der GC kann zudem recht zufällig vorgehen.
xtomcatx
 2008-09-11 13:04
#114526 #114526
User since
2006-08-27
31 Artikel
BenutzerIn
[default_avatar]
Spät in der Nacht bin ich gestern auf die Ursache des Problems gestoßen. Zuerst habe ich mir den Adapter für die Model-Schnittstelle gebaut. Hier war auch einiges grübeln angesagt, bis es endgültig dann doch recht einfach ging:
Code (perl): (dl )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Apache::Session::MySQL;

sub TIEHASH {
        my $class = shift;
        my $session_id = shift;
        my $param = shift;
        
        my $self = {};
        $self->{ASM} = Apache::Session::MySQL->TIEHASH( $session_id, {
                Handle => $param->{model}->{dbi},
                LockHandle => $param->{model}->{dbi},
                TableName => $param->{model}->{cfg}->{db}->param( "sessions" ) } );
        $param->{model}->{session} = $self;
        bless( $self, $class );
        return $self;
}

sub AUTOLOAD {
        my $self = shift;
        our $AUTOLOAD =~ s/.*:://;
        $self->{ASM}->$AUTOLOAD( @_ );
}

Vererbung funktioniert bei diesem Problem meiner Meinung nach nicht, wenn man nicht doch wieder alles an ASM abgeben will.

Als ich fertig war, war das Hauptproblem jedoch immer noch das selbe: DBI-Objekt wird zu früh zerstört, bzw. das Session-Objekt zu spät. Auch über einen zusätzlichen Destruktor für Model, der das registrierte Session zerstören sollte, hat es nicht funkitioniert. Allerdings war das Fehlerverhalten jetzt eindeutig reproduzierbar und nicht mehr so Dingen wie CGI::Carp abhängig, also konnte ich auf Fehlersuche gehen.

Lange Rede, kurzer Sinn: Der Fehler lag tatsächlich bei mir, allerdings kann ich ihn noch nicht so ganz nachziehen. Der relevante Code-Teil im Hauptprogramm:
Code (perl): (dl )
1
2
3
4
5
6
7
8
my $session = Session->new( { cgi => $cgi, model => $model } );
$session->create;
[...]
sub niegenutztesub {
        $session->header;
        tuetwas();
        exit;
}

Das Problem war nun, dass der lexikalische Gültigkeitsbereich von $session von mir ausgenutzt wurde. Ganz verstehen tu ichs nicht, weil die sub ja nirgends aufgerufen wird.
Ich vermute, dass der Compiler sich nicht sicher war, ob die sub vielleicht doch noch irgendwo aufgerufen wird und die Session-Instanz deshalb nicht zum normalen Zeitpunkt, sondern erst ganz am Schluss zerstört hat. Wieso nicht trotzdem das DBI-Objekt später zerstört wird, wo doch noch eine Referenz vorhanden ist, bleibt mir ein Rätsel.
Kurz gezögt habe ich, als ich auf die Übergabe von $session über einen Parameter verzichtet habe, aber habs dann doch so gemacht. Jedenfalls hat mich das jetzt gut einen Tag gekostet...
pq
 2008-09-11 15:47
#114534 #114534
User since
2003-08-04
12208 Artikel
Admin1
[Homepage]
user image
der compiler *kann* nicht sicher sein, ob das nochmal irgendwo aufgerufen wird.
hier wieder die generelle empfehlung: verwende keine globalen variablen, auch keine
"pseudo-globalen" mit my().
übergebe *immer* argumente an die subroutine. verhalte dich bei subroutinen so,
als ob sie in anderen packages stünden.
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live. -- Damian Conway in "Perl Best Practices"
lesen: Wiki:Wie frage ich & perlintro Wiki:brian's Leitfaden für jedes Perl-Problem
xtomcatx
 2008-09-11 17:22
#114541 #114541
User since
2006-08-27
31 Artikel
BenutzerIn
[default_avatar]
Ja, das tue ich eigentlich auch immer. Wie oben geschrieben, hatte ich schon ein ungutes Bauchgefühl, als ichs anders gemacht habe und es hat mich nicht getrogen :-)
Das nächste Mal höre ich wieder auf meinen Bauch, der weiß, was mit gut tut :-)
xtomcatx
 2008-09-11 20:19
#114546 #114546
User since
2006-08-27
31 Artikel
BenutzerIn
[default_avatar]
Gestern nacht hatte ich den Fehler wunderbar isoliert. Jetzt will ich ihn nochmal kontrolliert reproduzieren, weil ich ein Minimalbeispiel brauche, aber nun läuft auch in Situationen, die gestern meiner Meinung nach nicht funktioniert haben, alles astrein. Es ist zum aus der Haut fahren.
<< >> 8 Einträge, 1 Seite



View all threads created 2008-09-10 18:41.