error: maximum recursion depth exceeded

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
taake
User
Beiträge: 125
Registriert: Donnerstag 14. Oktober 2010, 08:49

Moin, ich hab ein Fehler im script und ich finde die Ursache dafür nicht.

Die Fehlermeldung sieht wie folgt aus:
rrd_insert_d( value )
File "scripts/temp_control.py", line 206, in rrd_insert_d
if os.path.isfile(rrd_grap_day) == True:
File "/usr/lib64/python2.7/genericpath.py", line 32, in isfile
return stat.S_ISREG(st.st_mode)
File "/usr/lib64/python2.7/stat.py", line 50, in S_ISREG
return S_IFMT(mode) == S_IFREG
RuntimeError: maximum recursion depth exceeded
Der betroffene Codeteil:

Code: Alles auswählen

def rrd_insert_d( value ):
        print 'function rrd_insert_d with:', value
        if os.path.isfile(rrd_grap_day) == True:
                ret = rrdtool.updatev(rrd_grap_day, 'N:%s' % (value))
                if ret:
                        print rrdtool.error()
        else:
                rrdtool.create(rrd_grap_day, "--start", str(int(time.time())), "--step", "60", "DS:temp0:GAUGE:90:U:U", "RRA:LAST:0.2:1:1440")
                rrd_insert_d( value )
Kann mir nicht erklären wie es bei der Überprüfunng ob die Datei da ist oder nicht zu "maximum recursion depth exceeded" kommen kann.

Das Problem tritt immer morgens meist so gegen 5/6uhr auf, wenn das hilft.

Edit: rrd_grap_day liegt unter /var/tempwatch/graph/daily.rrd
und ist ne Datei und kein symlink oder dergleichen.
BlackJack

@taake: Im ``else``-Zweig in `rrd_insert_d()` rufst Du `rrd_insert_d()` rekursiv auf. Und offenbar wird bei jedem Aufruf immer wieder dieser ``else``-Zweig betreten. Damit hast Du eine endlose Rekursion, beziehungsweise der Code läuft in diese Ausnahme, weil Python die Anzahl der rekursiven Aufrufe begrenzt bevor etwas schlimmeres passiert, wie zum Beispiel ein harter Programmabsturz weil der Aufrufstapelspeicher des Prozesses „überläuft”.

Als erstes stellt sich die Frage warum das überhaupt rekursiv formuliert ist. Dafür besteht kein Grund und man sollte in Sprachen wie Python nur Rekursion wenn das auch sinnvoll ist, weil andere Alternativen umständlicher und schwerer verständlich wären. Hier würde es völlig ausreichen und wäre konzeptionell einfacher, wenn man auf Vorhandensein der Datei prüft und wenn sie nicht vorhanden ist, sie anlegen lässt, und danach dann unbedingt das `updatev()` durchführt.

Nichtrekursiv formuliert wäre man das Problem mit der Endlosrekursion los. Bleibt die Frage warum die Datei nicht existiert. Kann es sein, dass der `create()`-Aufruf asynchron ist?

Vergleiche mit literalen Wahrheitswerten sind in aller Regel überflüssig und sollten weggelassen werden. `os.path.isfile()` liefert schon `True` oder `False`. Ein Vergleich mit `True` ergibt genau das selbe was `isfile()` schon geliefert hat. Der Vergleich hat also keinen sinvollen Effekt auf das Programm. Falls man das Gegenteil testen möchte, gibt es den ``not``-Operator.

Die Einrücktiefe ist zu hoch. Konvention sind vier Leerzeichen pro Ebene und keine Tabs.
taake
User
Beiträge: 125
Registriert: Donnerstag 14. Oktober 2010, 08:49

Vielen Dank für die Aufklärung,
jetzt kann ich das Problem nachvollziehen.
Habs jetzt dahingehend geändert das ich nur beim starten des scriptes 1x überprüfe ob die beiden datenbanken da sind und sie dann ggf. anlegt.
Macht auch irgendwie mehr Sinn, als das bei jedem Aufruf zu prüfen :)
Weiß ehrlich gesagt gar nicht mehr warum ich die Überprüfung bei jedem Aufruf gemacht hatte, irgendwas war mit dem bastel pc weswegen ich das gemacht hatte, aber ist jetzt auch nicht mehr nötig.

Ist das mit der Einrücktiefe schlimm, weil ich bis jetzt alles immer mit tabs gemacht habe.
Und bis dato auch noch nie Probleme dadurch bekommen habe.
Ehrlich gesagt wusste ich gar nicht das es dort unterschiede gibt.
BlackJack

@taake: Du kannst halt nicht einfach Code von woanders übernehmen oder jemand anders von Dir ohne das es zumindest unübersichtlich aussieht, weil Einrückung in Python ja eine Bedeutung hat. Das schliesst auch das zeigen von Quelltext hier im Forum mit ein, denn wenn jemand Deinen Code ausprobieren will um einen Fehler zu finden und ihn dazu verändern muss, dann verwenden 99% der Benutzer hier andere Editor-Einstellungen, die sich mit dem Quelltext von Dir nicht vertragen. Dabei können dann sogar Fehler entstehen die man dem Quelltext gar nicht ansieht. Da kann dann alles korrekt verschachtelt aussehen, aber der Compiler hat eine ganz andere Vorstellung von der Programmstruktur weil *der* den Unterschied zwischen Leerzeichen und Tabs macht, den man beim Betrachten des Quelltextes *nicht* sehen kann. Den umgekehrten Fall kann es auch geben: Für den Compiler stimmt die Einrückung semantisch, aber für den menschlichen Betrachter sieht der Quelltext falsch eingerückt aus.

Da ein Tab-Zeichen keine Information darüber enthält wo die Tabstops positioniert sind, das also in Verschiedenen Kontexten unterschiedlich sein kann und auch ist (Editor, Terminal, E-Mail, Webseite, …), haben sich die Python-Entwickler für Leerzeichen entschieden. Und da dann für vier pro Ebene als Kompromiss zwischen Lesbarbeit und Zeilenlängen.
taake
User
Beiträge: 125
Registriert: Donnerstag 14. Oktober 2010, 08:49

Nochmal danke für die Aufklärung, werde versuchen in Zukunft 4 Leerzeichen statt Tab zu nutzen.
Weil stimmt schon gerade bei irgendwelchen Verschachtelungen, kann es bei Tabs zu suboptimaler Lesbarkeit auf kleineren Bildschirmen kommen.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo.
taake hat geschrieben:Nochmal danke für die Aufklärung, werde versuchen in Zukunft 4 Leerzeichen statt Tab zu nutzen.
Weil stimmt schon gerade bei irgendwelchen Verschachtelungen, kann es bei Tabs zu suboptimaler Lesbarkeit auf kleineren Bildschirmen kommen.
Das ist gar nicht der Kern des Problems, das Aussehen ist erstmal egal. Das Problem ist, dass es für Tabs keine einheitliche länge gibt. Ob ein Tab nun als 8, 6, 4 Leerzeichen dargestellt wird, ist nicht festgelegt. Wenn du vier Leerzeichen machst, dann beträgt der Abstand immer 4 Leerzeichen, egal in was für einem Editor oder auf was für einem System. Problematisch wird es dann, wenn du Tabs und Leerzeichen mischt. Angenommen, du hast deinen Editor auf 8 Leerzeichen pro Tab eingestellt (so ist es auch bei Python definiert).Du kannst jetzt deinen Code schreiben und alles sieht wunderbar aus. Nun öffnet aber eine andere Person deinen Code, welche die Breite von Tabs auf 4 Leerzeichen gestellt hat. Dort wird man nichts mehr lesen können, da der ganze Code seltsam verschoben ist.

Aber auch anders herum ist es problematisch. Wenn ein Tab bei dir auf 4 Leerzeichen gestellt ist und du Leerzeichen und Tabs mischt, dann sieht dir Code bei dir im Editor perfekt aus. Da Python einen Tab aber als 8 Leerzeichen interpretiert, ist der Code für den Interpreter Müll.

Prinzipiell gilt: Verwende entweder Tabs oder Leerzeichen, aber niemals beides gemischt. PEP 8, welches du dir unbedingt durchlesen solltest, schlägt die Verwendung von 4 Leerzeichen pro Einrückungsebene vor. Am einfachsten ist es, wenn du deinen Editor so einstellst, dass er die Eingabe eines Tabs durch vier Leerzeichen ersetzt. Das sollte bei jedem vernünftigen Werkzeug gehen. Dann kannst du auch weiterhin wie gewohnt arbeiten.
Das Leben ist wie ein Tennisball.
taake
User
Beiträge: 125
Registriert: Donnerstag 14. Oktober 2010, 08:49

Danke erstmal dafür, habs jetzt in meine vimrc eingetragen das er für tab 4 leerzeichen macht.

Allerdings ist das eigentliche Problem noch existent.

Hab jetzt wohl auch den Grund dafür gefunden.
Allerdings weiß ich nicht wirklich wie ich das Problem lösen kann.

Das ganze script soll mir werte von ner Temperatursonde holden und die in ne rrd db packen.
Klappt auch alles soweit wie es soll.
Problem ist nur das ich zwei graphen füllen möchte, einmal ein täglichen mit minuten genauer Auflösung und einmal ein wöchtentlichen mit stunden genauer Auflösung.

Da ich das ganze in einem script erledigen wollte, habe ich zwei threads laufen die das erledigen sollen.
Allerdings nach dem wie ich das verstanden habe sind die threads für die "maximum recursion depth exceeded" verantwortlich.

Würde es etwas bringen die eigentlichen Funktionen die fürs Befüllen, Fehlerbehandlung, Meldung bei Überschreitung bestimmter Temperaturwerte in separate Dateien auslagere und in der "main" einfach nur die Threads und die timer für den aufruf der anderen scripts laufen lasse?
Oder würde dass das recursions Problem nur verzögern?
Das script soll beim start des rechners anlaufen und dann durchgehend laufen.

Jemand ne Idee wie ich das ganze händeln kann?
BlackJack

@taake: Da keiner weiss was das konkrete Problem ist, kann man schlecht eine gezielte Lösung vorschlagen. Denn Threads führen nicht grundsätzlich oder vermehrt zu Problemen mit Rekursionen. Es gibt halt die üblichen Probleme mit Nebenläufigkeit auf die man aufpassen muss.

Was läuft denn überhaupt in Threads bei Dir? Das befüllen der beiden Graphen doch wohl nicht, denn dazu sehe ich nicht, dass man die bräuchte. Da würde es reichen, wenn jeder 60. Wert nicht nur im täglichen Graphen sondern zusätzlich noch im wöchentlichen eingetragen wird.

Ansonsten muss man bei Threads darauf achten, dass es Ressourcen geben kann, die immer nur ein Thread gleichzeitig bearbeiten darf, weil es sonst Chaos gibt. Das erreicht man durch Sperren der betroffenen Codeabschnitte — das `threading`-Modul enthält dafür diverse Datentypen — oder in dem man den Zugriff grundsätzlich nur von *einem* Thread aus erledigt, dem die anderen Threads beispielsweise über eine Queue (`Queue.Queue` in der Standardbibliothek) Aufträge übergeben.
BlackJack

@taake: Zum Thema passend vielleicht folgender Link auf eine Seite der rrdtool-Homepage: http://oss.oetiker.ch/rrdtool/prog/rrdthreads.en.html

Das ganze auf mehrere Programme und damit Prozesse zu verteilen dürfte das Problem nicht lösen, es sei denn `rrdtool` garantiert auf dieser Ebene das gleichzeitiges Ändern von Dateien sicher ist.
taake
User
Beiträge: 125
Registriert: Donnerstag 14. Oktober 2010, 08:49

Danke für die Antworten, werde mich Montag wieder weiter mit befassen.
Das Problem scheint doch nicht auf den Threads zu beruhen, sondern am Aufbau selbst...

Hab mir ein paar Gedanken gemacht und die wohl "einfachste" Lösung wäre es das ganze event basiert zu machen.
Da das Problem wohl ist das ich am ende des Scripts wieder an den Anfang springe und mir die Zeit bis zum nächsten run hole.
Sehr warscheinlich kommt es genau dadurch zur rekursion.
Gibt es da irgendwas von python was mir sowas zur verfügung stellt, also minuten aufruf und stunden aufruf?

Werde wenns passt das ganze auch mal zusammenkürzen und hier posten, um zu verdeutlichen von was ich hier eigentlich erzähle :)
BlackJack

@taake: Falls Du mit „an den Anfang springen” meinst, dass Du eine Funktion aufrufst die bereits ausgeführt wird, und das immer wieder, ja dann liegt es daran. Ein Funktionsaufruf ist nicht einfach eine Sprunganweisung zu einer anderen Stelle im Code. Es wird sich gemerkt von wo gesprungen wurde, damit der Code dort weiter ausgeführt werden kann wenn die aufgerufene Funktion die Kontrolle durch ``return`` oder das Ende der Funktion abgibt. Und es werden auch alle lokalen Namen und Werte von jedem Aufruf solange aufgehoben bis der Funktionsaufruf zuende abgearbeitet wurde. Wenn eine aufgerufene Funktion fertig ist und die Kontrolle wieder zum Aufrufer wandert, möchte man mit diesen Werten ja vielleicht dort weiterarbeiten. Bei einer Endlosrekursion sammeln sich so immer mehr Daten auf dem Aufrufstapel an und irgendwann würden die andere Speicherbereiche überschreiben und das Programm macht sonderbare Sachen und/oder stürzt ab. Um dem vorzubeugen gibt es das Rekursionslimit bei dem die Python-Laufzeitumgebung eine Ausnahme auslöst, um schlimmeres zu verhindern.

Wenn Du eine Endlosschleife haben möchtest, dann sollte das eine iterative Schleife sein, im einfachsten Fall einfach ``while True:``, und kein rekursiver Aufruf.

Edit: Das `sched`-Modul aus der Standardbibliothek ist vielleicht interessant für Dich.
taake
User
Beiträge: 125
Registriert: Donnerstag 14. Oktober 2010, 08:49

Danke werde ich mir gleich mal anschauen,

das hatte ich mir mitlerweile auch gedacht das es daran liegt.

Ich poste einfach mal den zusammengekürzen source.
Der deine Vermutung bestätigt, muss jetzt nur schauen wie ich das umschiffe.
Im Moment läuft es zwar aber in zwei scripten die über nen cronjob aufgerufen werden und das möchte ich nicht, will ja lernen wies geht.

Code: Alles auswählen

#!/usr/bin/python2.7
# -*- coding: iso-8859-15 -*-

import time
import threading
import datetime

timeformat = '%d-%m-%Y %H:%M:%S'

def main():
    timer_daily_thread = threading.Thread(target=timer_daily, args=())
    timer_weekly_thread = threading.Thread(target=timer_weekly, args=())
    timer_daily_thread.start()
    timer_weekly_thread.start()
    timer_daily_thread.join()
    timer_weekly_thread.join()


def timer_daily():
    print 'timer_daily start', time.strftime(timeformat)
    time.sleep(time_to_next_minute())
    check_temp('daily')

def timer_weekly():
    print 'timer_weekly start', time.strftime(timeformat)
    time.sleep(time_to_next_hour())
    check_temp ('weekly')


def time_to_next_hour():
    now = datetime.datetime.now().replace(microsecond=0)
    plus1 = now + datetime.timedelta(hours=1)
    time_clear = plus1.replace(minute=0,second=0,microsecond=0)
    time_to_hour = time_clear - now
    return time_to_hour.total_seconds()


def time_to_next_minute(): 
    now = datetime.datetime.now().replace(microsecond=0)
    plus1 = now + datetime.timedelta(minutes=1)
    time_clear = plus1.replace(second=0,microsecond=0)
    time_to_minute = time_clear - now
#    return time_to_minute.total_seconds()
    return 1 # faster run into recursion error

def get_current_temp(): # dumy function
    import random
    temp1 = random.randint(20,25)
    temp2 = random.randint(10,50)
    return str(temp1)+'.'+str(temp2)


def check_temp(arg):
    temperatur = get_current_temp()
    rrd_switch(arg, temperatur)

def rrd_switch(arg, value):
    if arg == 'daily':
        rrd_insert_d(value)
    if arg == 'weekly':
        rrd_insert_w(value)


def rrd_insert_d(value):
    print 'function rrd_insert_d with:', value
    timer_daily()

def rrd_insert_w(value):
    print 'function rrd_insert_w with:', value
    timer_weekly()


if __name__=="__main__":
    main()
Wie du schon meintest hab ich auch

def rrd_insert_d( value ):
print 'function rrd_insert_w with:', value
timer_daily()

als fehlerquelle identifiziert, hoffentlich krieg ich es mit deinen tipps hin das zu umschiffen
while true sollte funktionieren, allerdings wäre das eher ne dreckige lösung also eher die notlösung werde mir 'sched' module mal anschauen.
taake
User
Beiträge: 125
Registriert: Donnerstag 14. Oktober 2010, 08:49

@BlackJack: Mal wieder vielen Dank.

Sieht ganz gut aus, läuft jetzt im Testbetrieb seit 30 min ohne recursionsproblem

Code: Alles auswählen

#!/usr/bin/python2.7
# -*- coding: iso-8859-15 -*-

import time
import threading
import datetime
import sched

timeformat = '%d-%m-%Y %H:%M:%S'
scheduler = sched.scheduler(time.time, time.sleep)

def main():
    timer_daily_thread = threading.Thread(target=timer_daily, args=())
    timer_weekly_thread = threading.Thread(target=timer_weekly, args=())
    timer_daily_thread.start()
    timer_weekly_thread.start()
    timer_daily_thread.join()
    timer_weekly_thread.join()



def timer_daily():
    print 'timer_daily start', time.strftime(timeformat)
    time.sleep(time_to_next_minute())
    scheduler_daily()

def timer_weekly():
    print 'timer_weekly start', time.strftime(timeformat)
    time.sleep(time_to_next_hour())
    scheduler_weekly()

def scheduler_daily():
    print 'daily_scheduler start', time.strftime(timeformat)
    scheduler.enter(1, 1, scheduler_daily, ())
    check_temp('d')

def scheduler_weekly():
    print 'weekly_scheduler start', time.strftime(timeformat)
    scheduler.enter(3, 1, scheduler_weekly, ())
    check_temp('w')

def time_to_next_hour():
    now = datetime.datetime.now().replace(microsecond=0)
    plus1 = now + datetime.timedelta(hours=1)
    time_clear = plus1.replace(minute=0,second=0,microsecond=0)
    time_to_hour = time_clear - now
#    return time_to_hour.total_seconds()
    return 2

def time_to_next_minute(): 
    now = datetime.datetime.now().replace(microsecond=0)
    plus1 = now + datetime.timedelta(minutes=1)
    time_clear = plus1.replace(second=0,microsecond=0)
    time_to_minute = time_clear - now
#    return time_to_minute.total_seconds()
    return 1 # faster run into recursion error

def get_current_temp():
    import random
    temp1 = random.randint(20,25)
    temp2 = random.randint(10,50)
    return str(temp1)+'.'+str(temp2)


def check_temp(arg):
    print 'check temp'
    temperatur = get_current_temp()
    rrd_switch(arg, temperatur)
    
def rrd_switch(arg, value):
    if arg == 'd':
        rrd_insert_d(value)
    if arg == 'w':
        rrd_insert_w(value)


def rrd_insert_d( value ):
    print 'function rrd_iinsert_d with:', value, time.strftime(timeformat)

def rrd_insert_w( value ):
    print 'function rrd_insert_w with:', value, time.strftime(timeformat)


if __name__=="__main__":
    main()

scheduler.run()
Wenn ich es richtig verstanden habe dann wartet sched mit der ausführung von z.B. scheduler_weekly()
bis scheduler_daily() durchgelaufen ist (wenn er ihn gerade abhandelt) und umgekehrt oder?
BlackJack

@taake: Folgendes ist noch zu deinem Vorletzen Beitrag:

Da sind für meinen Geschmack ein bisschen viele Quelltextwiederholungen und das mit `check_temp()` und `rrd_switch()` sieht nach unnötiger Indirektion aus. *Da* hätte man den Aufruf von `get_current_temp()` durchaus mal in den beiden `timer_*()`-Funktionen wiederholen können und dann gleich von dort die passende Funktion zum Eintragen in die RRD-DB aufrufen können.

Von den drei Threads die das Programm hat, tun nur zwei tatsächlich etwas. Der Hauptthread tut nach dem starten der beiden anderen gar nichts sinnvolles mehr.

Wie weiter oben schon erwähnt: Die Threads sind eigentlich überflüssig. Denn jede Stunde fragen die zum gleichen Zeitpunkt zweimal die Temperatur ab, also eigentlich zweimal den gleichen Wert. Da kann man auch alle Minute die Temperatur abfragen und alle 60 Minuten den Wert zusätzlich in die wöchentliche Zeitreihe schreiben.

Nun zum letzten Beitrag: Die Threads sind hier total überflüssig. Du startest die nur um damit jeweils den *ersten* Eintrag in die Queue vom Scheduler zu tätigen. Danach sind die Threads beendet. Sonst würde der Scheduler auch gar keine Funktionen aufrufen, denn dessen `run()`-Methode wird ja erst ausgeführt nachdem die Threads beendet wurden.

Hier mal ein Versuch das ohne zusätzliche Threads und mit nur einer `action`-Funktion und ohne globaler Scheduler-Variable umzusetzen:

Code: Alles auswählen

#!/usr/bin/env python
from calendar import timegm
from datetime import datetime as DateTime, timedelta as TimeDelta
from random import randint
from sched import scheduler as Scheduler
from time import sleep, time


ONE_MINUTE = TimeDelta(minutes=1)


def date2timestamp(date):
    return timegm(date.timetuple())


def get_temperature():
    return '{0}.{1}'.format(randint(20, 25), randint(10, 50))


def rrd_insert_daily(date, value):
    print 'rrd daily {0}: {1}'.format(date, value)


def rrd_insert_weekly(date, value):
    print 'rrd weekly {0}: {1}'.format(date, value)


def measure_action(scheduler, timestamp):
    date = DateTime.utcfromtimestamp(timestamp)
    temperature = get_temperature()

    rrd_insert_daily(date, temperature)
    if date.minute == 0:
        rrd_insert_weekly(date, temperature)
    
    next_minute = date2timestamp(date + ONE_MINUTE)
    scheduler.enterabs(next_minute, 0, measure_action, (scheduler, next_minute))


def main():
    scheduler = Scheduler(time, sleep)
    next_minute = date2timestamp(
        (DateTime.utcnow() + ONE_MINUTE).replace(second=0, microsecond=0)
    )
    scheduler.enterabs(next_minute, 0, measure_action, (scheduler, next_minute))
    scheduler.run()


if __name__ == '__main__':
    main()
taake
User
Beiträge: 125
Registriert: Donnerstag 14. Oktober 2010, 08:49

Oh man jetzt fühle ich mich als hätte ich python vergewaltigt wenn ich mir anschaue wie elegant sich dein source liest.

Vielen Dank für die Vorlage (werde ich gleich mal speichern), werde zwar nicht alles übernehmen, sonst hab ich nachher ggf. probleme mit der Wartung, da liegen gefühlt Welten zwischen unseren "skills".

'check_temp()' ist im eigentlich script nicht ganz so sinnlos, da dort noch ein paar prüfungen über den rückgabewert von 'get_current_temp' laufen.
Aber das geht aus der verkürzten version natürlich nicht hervor.

Allerdings werde ich anhand deines sources gleich mal die threads rauswerfen und den das ganze wie du in einem insert unterbringen.

Nochmals vielen dank für Hilfe.


edit: hab doch fast alles von dir implementiert, ist einfach besser :)
Antworten