Regression with Streamlit 1.38.0 in comparison with 1.34.0

Hello,

I’m using Streamlit 1.38.0 and I have a regression in comparison with streamlit 1.34.0.
For an unknown reason, the streamlit script is automatically restarted when executing a user action not using a component.

The specificity of this action is to call an external program with the python package “pexpect”.

Generally the external program is called 2 times (script restart issued by Streamlit itself) but it can be called only 1 time (without automatic restart).

The result is the same with subprocess.open instead of pexpect.spawn.

My code:

def run(cmd, pattern=None, gson=True):
if ‘cptrun’ not in st.session_state: st.session_state.cptrun = -1
st.session_state.cptrun = st.session_state.cptrun + 1
if st.session_state.cptrun > 0: logger.critical(f’Command launched several times: {cmd}')
logger.debug(cmd)
child = pexpect.spawn(cmd, encoding=‘utf8’, timeout=None)
x = ‘’.join([line for line in child])
logger.debug(x)
st.session_state.cptrun = -1
if gson:
result = json.loads(x)
if pattern: result = jq(pattern).transform(result)
else:
result = x
return result

The problem doesn’t exist with streamlit 1.34.0 !

What has been changed between these 2 versions explaining an automatic restart of the script when multi process are involved ?

Regards

There is a general question on how to manage Streamlit restarts.
Streamlit should indicate in a variable (may be within st.sessions_state) the reason of the restart.

In my mind there are 3 reasons:

a) usage of st.rerun()
b) component reinstantiation
c) other reason ==> this is my case

How are you calling this function?

Thank you for taking the time to try to understand my problem.

a) I was unable to develop a test case that illustrates my problem outside of my context.
b) My context is complex. I have a Streamlit application with three columns: in the left and right columns, there are two menus (which are external components that I defined), and in the middle column, there is another external component (defined by me) that represents a tree structure.

When selecting an option from the right menu, I call a function that creates an element that will appear in the middle tree structure.

Until Streamlit version 1.34.0, everything was fine; I didn’t observe any particular side effects.

Using Streamlit version 1.38.0, the problem appeared: the function that creates an element is often called twice, but sometimes only once, as before. Using logs, I realized that the script undergoes a “rerun” in the middle of the function that creates the element.

A piece of code showing how the “kairos_create_node” function is called:

                m2 = MENU(menu=menu, height=600, mode='inline', theme=theme['streamlit_theme'], id='options')
                if m2.get(): 
                    st.session_state.nodemenu = m2.get()
                    logger.debug(f'Nodemenu option: {st.session_state.nodemenu}')

            if 'nodemenu' not in st.session_state: st.session_state.nodemenu = None
            if st.session_state.nodemenu == "Create node": kairos_create_node()

Now the “kairos_create_node” function:

def kairos_create_node():
    nodesdb = st.session_state.nodesdb
    node = st.session_state.node
    logger.info(f'Creating a new node into {nodesdb} and attaching node to {node}')
    command = f'kairos -s createnode --nodesdb {nodesdb} --id {node}'
    act = run(command)
    logger.debug(act)
    if 'success' in act and act['success']:
        st.session_state.treeupdated = True
        st.session_state.nodemenu = None
        run_again()
    else: st.error(act['message'])

and now the “run function”:

def run(cmd, pattern=None, gson=True):
    if 'cptrun' not in st.session_state: st.session_state.cptrun = -1
    st.session_state.cptrun = st.session_state.cptrun + 1
    if st.session_state.cptrun > 0: logger.critical(f'Command launched several times: {cmd}')
    logger.debug(cmd)
    child = pexpect.spawn(cmd, encoding='utf8', timeout=None)
    x = ''.join([line for line in child])
    logger.debug(x)
    st.session_state.cptrun = -1
    if gson:
        result = json.loads(x)
        if pattern: result = jq(pattern).transform(result)
    else:
        result = x
    return result

The peculiarity of the “run” function called by “kairos_create_node” is that it uses “pexpect.spawn,” which is equivalent to “subprocess.check_output” and, by the way, causes the same issue. Here, we execute a command through a subprocess and want to capture the result in a variable.

The logs set up in my context show that the “rerun” initiated by Streamlit version 1.38.0 occurs inside the “run” function, likely due to the specifics related to the use of a subprocess.

An exemple of trace illustrating the problem:

2024-08-29 09:57:22.025 | DEBUG    | __main__:<module>:66 - Selected node : 1
2024-08-29 09:57:38.928 | DEBUG    | __main__:<module>:7 - Generating the KAIROS page ...
2024-08-29 09:57:38.931 | DEBUG    | __main__:<module>:35 - Mainmenu option: None
2024-08-29 09:57:38.935 | DEBUG    | __main__:<module>:66 - Selected node : 1
2024-08-29 09:57:38.936 | DEBUG    | pages.functions.definitions:run_again:23 - Restarting SCRIPT ...
2024-08-29 09:57:39.005 | DEBUG    | __main__:<module>:7 - Generating the KAIROS page ...
2024-08-29 09:57:39.009 | DEBUG    | __main__:<module>:35 - Mainmenu option: None
2024-08-29 09:57:39.012 | DEBUG    | __main__:<module>:66 - Selected node : 1
2024-08-29 09:57:39.013 | DEBUG    | __main__:<module>:121 - Nodemenu option: Create node
2024-08-29 09:57:39.013 | INFO     | pages.functions.definitions:kairos_create_node:178 - Creating a new node into kairos_user_admin and attaching node to 1
2024-08-29 09:57:39.014 | DEBUG    | pages.functions.definitions:run:495 - kairos -s createnode --nodesdb kairos_user_admin --id 1
2024-08-29 09:57:39.548 | DEBUG    | __main__:<module>:7 - Generating the KAIROS page ...
2024-08-29 09:57:39.552 | DEBUG    | __main__:<module>:35 - Mainmenu option: None
2024-08-29 09:57:39.560 | DEBUG    | __main__:<module>:66 - Selected node : 1
2024-08-29 09:57:39.563 | INFO     | pages.functions.definitions:kairos_create_node:178 - Creating a new node into kairos_user_admin and attaching node to 1
2024-08-29 09:57:39.563 | CRITICAL | pages.functions.definitions:run:494 - Command launched several times: kairos -s createnode --nodesdb kairos_user_admin --id 1
2024-08-29 09:57:39.564 | DEBUG    | pages.functions.definitions:run:495 - kairos -s createnode --nodesdb kairos_user_admin --id 1
2024-08-29 09:57:39.813 | DEBUG    | pages.functions.definitions:run:498 - {

    "data": {

        "created": "2024-08-29 09:57:39.745",

        "datasource": {

            "cache": {},

            "producers": [],

            "type": "N"

        },

        "icon": "N",

        "id": 279,

        "name": "2024-08-29 09:57:39.743",

        "status": "ACTIVE"

    },

    "success": true

}


2024-08-29 09:57:40.214 | DEBUG    | pages.functions.definitions:run:498 - {

    "data": {

        "created": "2024-08-29 09:57:40.167",

        "datasource": {

            "cache": {},

            "producers": [],

            "type": "N"

        },

        "icon": "N",

        "id": 280,

        "name": "2024-08-29 09:57:40.165",

        "status": "ACTIVE"

    },

    "success": true

}


2024-08-29 09:57:40.315 | DEBUG    | pages.functions.definitions:kairos_create_node:181 - {'data': {'created': '2024-08-29 09:57:40.167', 'datasource': {'cache': {}, 'producers': [], 'type': 'N'}, 'icon': 'N', 'id': 280, 'name': '2024-08-29 09:57:40.165', 'status': 'ACTIVE'}, 'success': True}
2024-08-29 09:57:40.315 | DEBUG    | pages.functions.definitions:run_again:23 - Restarting SCRIPT ...
2024-08-29 09:57:40.406 | DEBUG    | __main__:<module>:7 - Generating the KAIROS page ...
2024-08-29 09:57:40.409 | DEBUG    | __main__:<module>:35 - Mainmenu option: None

Some explanation:

At the beginning og the script I have one instruction which shows when the script is rerun:

In the log we have:

2024-08-29 09:57:40.406 | DEBUG | main::7 - Generating the KAIROS page …

Sometimes we have:

2024-08-29 09:57:38.936 | DEBUG | pages.functions.definitions:run_again:23 - Restarting SCRIPT …
2024-08-29 09:57:39.005 | DEBUG | main::7 - Generating the KAIROS page …

This case occurs when st.rerun() has been called explicitely by the script.

BUT when

2024-08-29 09:57:39.005 | DEBUG | main::7 - Generating the KAIROS page …

is not preceded by

2024-08-29 09:57:38.936 | DEBUG | pages.functions.definitions:run_again:23 - Restarting SCRIPT …

that means that the rerun has been initiated by Streamlit itself…

So when you analyze the log, you are convinced that the “rerun” initiated by streamlit occurs between these instructions appearing in the ‘run’ function:

logger.debug(cmd)
child = pexpect.spawn(cmd, encoding=‘utf8’, timeout=None)
x = ‘’.join([line for line in child])
logger.debug(x)

So the question is: what are the updates done in Streamlit code between 1.34.0 and 1.38.0.

I’m convinced that one of these updates are in strong relationship with the problem I observed.

Of course, I can try each Streamlit version 1.35.0, … to isolate the version showing the change but not yet done…

An other information: I tried to put the “st.session_state.nodemenu = None” earlier in the script but the side effect occurs differently.

There is something associated with the subprocess that causes Streamlit to restart the script.

It would be great if Streamlit provided a log explaining the reason for a “rerun.”

I tried all versions between 1.34.0 and 1.38.0. The problem appears with version 1.37.0.

So what has changed between 1.36.0 and 1.37.0 explaining theses extra “reruns”

I have modified the log in order to add the thread number in which the trace has been issued:

2024-08-29 11:45:56.156 | 140055299880704 | DEBUG    | __main__:<module>:7 | - Generating the KAIROS page ...
2024-08-29 11:45:56.158 | 140055299880704 | DEBUG    | __main__:<module>:35 | - Mainmenu option: None
2024-08-29 11:45:56.161 | 140055299880704 | DEBUG    | __main__:<module>:66 | - Selected node : 1
2024-08-29 11:45:56.162 | 140055299880704 | DEBUG    | __main__:<module>:121 | - Nodemenu option: Create node
2024-08-29 11:45:56.162 | 140055299880704 | INFO     | pages.functions.definitions:kairos_create_node:178 | - Creating a new node into kairos_user_admin and attaching node to 1
2024-08-29 11:45:56.162 | 140055299880704 | DEBUG    | pages.functions.definitions:run:495 | - kairos -s createnode --nodesdb kairos_user_admin --id 1
2024-08-29 11:45:56.409 | 140055350220544 | DEBUG    | __main__:<module>:7 | - Generating the KAIROS page ...
2024-08-29 11:45:56.411 | 140055350220544 | DEBUG    | __main__:<module>:35 | - Mainmenu option: None
2024-08-29 11:45:56.425 | 140055350220544 | DEBUG    | __main__:<module>:66 | - Selected node : 1
2024-08-29 11:45:56.426 | 140055350220544 | INFO     | pages.functions.definitions:kairos_create_node:178 | - Creating a new node into kairos_user_admin and attaching node to 1
2024-08-29 11:45:56.427 | 140055350220544 | CRITICAL | pages.functions.definitions:run:494 | - Command launched several times: kairos -s createnode --nodesdb kairos_user_admin --id 1
2024-08-29 11:45:56.427 | 140055350220544 | DEBUG    | pages.functions.definitions:run:495 | - kairos -s createnode --nodesdb kairos_user_admin --id 1
2024-08-29 11:45:56.856 | 140055299880704 | DEBUG    | pages.functions.definitions:run:498 | - {

    "data": {

        "created": "2024-08-29 11:45:56.799",

        "datasource": {

            "cache": {},

            "producers": [],

            "type": "N"

        },

        "icon": "N",

        "id": 297,

        "name": "2024-08-29 11:45:56.770",

        "status": "ACTIVE"

    },

    "success": true

}


2024-08-29 11:45:57.036 | 140055350220544 | DEBUG    | pages.functions.definitions:run:498 | - {

    "data": {

        "created": "2024-08-29 11:45:56.987",

        "datasource": {

            "cache": {},

            "producers": [],

            "type": "N"

        },

        "icon": "N",

        "id": 298,

        "name": "2024-08-29 11:45:56.985",

        "status": "ACTIVE"

    },

    "success": true

}


2024-08-29 11:45:57.137 | 140055350220544 | DEBUG    | pages.functions.definitions:kairos_create_node:181 | - {'data': {'created': '2024-08-29 11:45:56.987', 'datasource': {'cache': {}, 'producers': [], 'type': 'N'}, 'icon': 'N', 'id': 298, 'name': '2024-08-29 11:45:56.985', 'status': 'ACTIVE'}, 'success': True}
2024-08-29 11:45:57.138 | 140055350220544 | DEBUG    | pages.functions.definitions:run_again:23 | - Restarting SCRIPT ...
2024-08-29 11:45:57.205 | 140055350220544 | DEBUG    | __main__:<module>:7 | - Generating the KAIROS page ...
2024-08-29 11:45:57.207 | 140055350220544 | DEBUG    | __main__:<module>:35 | - Mainmenu option: None
2024-08-29 11:45:57.210 | 140055350220544 | DEBUG    | pages.functions.definitions:run:495 | - kairos -s getwholetree --nodesdb kairos_user_admin
2024-08-29 11:45:57.706 | 140055350220544 | DEBUG    | pages.functions.definitions:run:498 | - {

    "data": {

        "1": {

            "name": "/",

            "parent": null,

            "type": "N"

        },

        "2": {

            "name": "Trash",

            "parent": 1,

            "type": "T"

        },

        "293": {

            "name": "2024-08-29 10:30:52.109",

            "parent": 1,

            "type": "N"

        },

        "294": {

            "name": "2024-08-29 10:30:52.359",

            "parent": 1,

            "type": "N"

        },

        "295": {

            "name": "2024-08-29 11:43:35.122",

            "parent": 1,

            "type": "N"

        },

        "296": {

            "name": "2024-08-29 11:43:35.457",

            "parent": 1,

            "type": "N"

        },

        "297": {

            "name": "2024-08-29 11:45:56.770",

            "parent": 1,

            "type": "N"

        },

        "298": {

            "name": "2024-08-29 11:45:56.985",

            "parent": 1,

            "type": "N"

        }

    },

    "success": true

}


2024-08-29 11:45:57.809 | 140055350220544 | DEBUG    | __main__:<module>:66 | - Selected node : 1

In this example, at the beginning you have thread# 140055299880704. After a while you have thread# 140055350220544 and you can see the 2 threads continuing their work after…

Your application is a test case. But complexity makes it hard to identify the issue. I suggest you try to simplify it as much as possible while still being able to reproduce the problem.

There are at least a couple of commits related to reruns.

See also:

I have simplified the script, but I am still not able to produce a test case that is independent of my context.

However, I have some additional information with this problem: In my script I’m using the “st_theme” function in order to get default theme “light” or “dark”.

If I remove this call, everything is OK.

So if I resume the case:

To reproduce this problem, I need to have:

a) Streamlit V1.37.0
b) I need to run pexpect.call
c) I need to call st_theme
d) The call must be done through a user component.

My actual simplified script is the following:

import streamlit as st
from pages.functions.definitions import *

theme = set_page_config()
#theme = dict(streamlit_theme='light')

logger.debug('Generating the KAIROS page ...')
st.session_state.user = "admin"
st.session_state.password = "admin"
st.session_state.adminrights = True
st.session_state.nodesdb = "kairos_user_admin"
st.session_state.node = 1
st.session_state.type = "N"
menu = []
menu.append({'label': 'Manage nodes', 'children': [
    {'label':'Create node', 'key':"Create node", 'disabled': False},
]})

m2 = MENU(menu=menu, height=600, mode='inline', theme=theme['streamlit_theme'], id='options')
if m2.get(): 
    st.session_state.nodemenu = m2.get()
    logger.debug(f'Nodemenu option: {st.session_state.nodemenu}')

b1 = st.button("Create node")
if b1: st.session_state.nodemenu = "Create node"

if 'nodemenu' not in st.session_state: st.session_state.nodemenu = None
if st.session_state.nodemenu == "Create node": kairos_create_node()

With this script, I can reproduce the problem