New library: streamlit-server-state, a new way to share states across sessions on the server

Hi community,

I created streamlit-server-state, through which apps can use a state object shared across the sessions on the server with an interface similar to the SessionState.

One reason why I developed this is to create chat apps.
I think chat apps are one kind of applications good for NLP methods to be used with, though they cannot be implemented upon raw Streamlit since different users (sessions) cannot communicate with each other.

With this library, sessions have access to a “server-wide” shared state object and I could actually create a simple chat app; https://share.streamlit.io/whitphx/streamlit-server-state/main/app_chat_rooms.py

  1. You can enter an existing room or create a new one to join.
  2. First you set your nick name.
  3. Then, you can send messages in the room.
    streamlit-app_chat_rooms-2021-07-14-20-07-84

One interesting point of this library is that once the code refers to the server-state, the session automatically starts to subscribe the state and when the state is updated within one session, all the subscribing sessions are automatically rerun.
This makes it possible for all the clients in a chat app to automatically load the messages when a new message is added.

I think for example combining sentiment analysis to chat apps will be interesting, or auto-assigning emoji to each message with DeepMoji is another application which I think will be fun… These are future work, or can anyone please do them? :wink:


Note that this library uses not public API of Streamlit, breaks the built-in session isolation mechanism to some extent and might introduce some vulnerability into the app. I recommend only to use this library for private prototypes.

In addition, the sample chat app above does not have authentication mechanism and I did not care about security. Do not share sensitive information there.

18 Likes

Hi!

Appreciate for your creation! Without which I wouldn’t be able to write something fun like this Gomoku game.

Only a minor problem I found when using this amazing library, which is things sometimes doesn’t work as automatically rerun the script once the subscribed state updated, as I found in this chat room demo, creating rooms DO sync across sessions, but not so with sending a message (It seems to trigger the rerun but the new message does NOT display in the other sessions). It seems to me that it only works on subscribing the server state with variables that are not built-in session state. Due to the same reason, I had to use a blocking while loop to detect if the server state changes for things to work properly.

Could you please take some time investigate into this problem? Thanks again for your hard work!!

@TeddyHuang-00 Hi, thank you for reporting it with detailed descriptions. I will check the problem :ok_man:


UPDATE:
I found that the server-state does not work properly with Streamlit==0.89.0. If you are using this version, can you try downgrading?
I will also fix the problem with v0.89.0.

Thanks a lot for your advice! I’ll try it out later and reply to you as soon as I got any result


UPDATE:

I’ve modified my code a little bit, but it doesn’t seem to be working… I’ll do some further test and may help see what’s going wrong…

@whitphx Hi, I did some tests using code snippets like following:

# # Initialize the server state (srs) and session state (sss)

# Modify the server state
if slt.button("Server list append"):
    with server_state_lock["srs_lst"]:
        server_state["srs_lst"].append(0)
        # Does NOT trigger a rerun in other sessions
if slt.button("Server value add"):
    with server_state_lock["srs_val"]:
        server_state["srs_val"] += 1
        # DOES trigger a rerun in other sessions

# # some subscribing

# # Display variables

I don’t really know what’s going wrong, but it seems like that iterable object like list and dict in server state couldn’t be tracked properly, and once some non-iterable variables like int or float is modified, the server state is able to trigger a script rerun thus sync across sessions.

BTW, in my simple test, the Streamlit version doesn’t matter. 0.89.0 and 0.88.0 give the same behavior, except for in 0.88.0, updating such non-iterable object in server state will also give a warning in the command line Discarding ScriptRequest.RERUN request after shutdown, while manipulating iterable doesn’t do so.

For a quick workaround in the script, a naive way is to add a counter directly in the server state like:

# Initialize the counter state
if "room-uuid" not in server_state:
    with server_state_lock["room-uuid"]:
        server_state["room-uuid"] = 0

# Do something with server state

# Modify the counter to rerun corresponding sessions
with server_state_lock["room-uuid"]:
    server_state["room-uuid"] += 1

this will rerun other sessions no matter what type of variable is being changed in the server state.

Hope this will help you to debug and others who are using this library!

@TeddyHuang-00 Thank you very much for the detailed report.

I don’t really know what’s going wrong, but it seems like that iterable object like list and dict in server state couldn’t be tracked properly, and once some non-iterable variables like int or float is modified, the server state is able to trigger a script rerun thus sync across sessions.

This is true. It’s an expected but undocumented behavior, sorry.

More precisely, streamlit-server-state does not detect “mutations” on mutable objects, including iterables like list and also others like dict.

Under the hood, the server state triggers rerunning when values are set through key-based or attr-based accesses like server_state["foo"] = 42 or server_state.foo = 42, which is implemented via magic methods __setattr__() and __setitem__().

In your example,

server_state["srs_val"] += 1

is a shorthand of

server_state["srs_val"] = server_state["srs_val"] + 1

so can be detected with this mechanism.

In contrast, however,

server_state["srs_lst"].append(0)

is just a mutation or a method call of the list object, which cannot be detected to trigger rerunning.

So, please fix the current usage to avoid this problem for now.
For instance, base on your example, when dealing with mutable objects like list,
do:

if slt.button("Server list append"):
    with server_state_lock["srs_lst"]:
        server_state["srs_lst"] += [0]  # server_state["srs_lst"] = server_state["srs_lst"] + [0]

or

if slt.button("Server list append"):
    with server_state_lock["srs_lst"]:
        srs_lst = server_state["srs_lst"]
        srs_lst.append(0)
        server_state["srs_lst"] = srs_lst

FYI, I’m thinking about introducing a new mechanism to detect such mutations by wrapping the objects with “proxy” objects. This PR is a prototype of that. However, I cannot be sure this is good to go as this option has many disadvantages.

1 Like

Thanks a lot for your explanation! And I could fully understand what’s going on now. Thanks again for your amazing library and I’m really looking forward to the improvement! :smiley:

In addition to that, I also found other bugs during inspection.

One is a regression of this issue from Streamlit==0.86.0.

Another is this one.

Both are related to components like radio or text_input, or maybe also to session_state.

Please also track these bug reports if you think they are related to your problem.

FYI, The issue about v0.89.0 which I have reported was caused by the second one. You may find that the chat room example does not work properly with v0.89.0, where the message log disappears after sending a new message.

1 Like

@whitphx Thanks with that, I was able to found something that causes everything in my code to go wrong.

TL;DR: object in server_state can NOT trigger a session rerun using the same method __setitem__, while assigning a new or deep-copied object does the trick. This may relate to the default behavior of __setitem__ and __setattr__.

I don’t know a lot about Python, but I think the problem is that a shallow copy of the object still refers to the original one, thus not changing the address of it. And the __setitem__ (and very likely __setattr__, not tested though) might check on the values before really doing the job, which is that it checks if the values matches the previous ones, and unfortunately if they are the same (like the unchanging address of the object), it will just pass on and do nothing. A quick fix to this problem is using a deepcopy of the original object and assign this copy back to where it from (this will trigger a rerun no matter the attributes of the object change or not) as shown in the following code snippet.

Here is the code I used for testing, and putting class object directly inside server_state gives the same result:

from copy import deepcopy
import streamlit as slt
from streamlit_server_state import server_state, server_state_lock


class someClass:
    def __init__(self):
        self.x = 0
    def increment(self):
        self.x += 1


if "DIC" not in server_state:
    server_state.DIC = {"Value": 0, "Object": someClass()}

if slt.button("Reset"):
    with server_state_lock["DIC"]:
        server_state.DIC["Value"] = 0
        server_state.DIC["Object"] = someClass()
if slt.button("Value Increment"):
    # This works, just as the way you suggest and as it should be
    with server_state_lock["DIC"]:
        temp_state = server_state.DIC
        temp_state["Value"] += 1
        server_state.DIC = temp_state
if slt.button("Object Increment"):
    # This does NOT work
    with server_state_lock["DIC"]:
        temp_state = server_state.DIC
        temp_state["Object"].increment()
        server_state.DIC = temp_state
if slt.button("Object Increment with deepcopy"):
    # This works though
    with server_state_lock["DIC"]:
        temp_state = server_state.DIC
        temp_state["Object"] = deepcopy(temp_state["Object"])
        temp_state["Object"].increment()
        server_state.DIC = temp_state

slt.write(server_state.DIC["Value"])
slt.write(server_state.DIC["Object"].x)
slt.write(server_state.DIC["Object"])

UPDATE: Too embarrassed for this problem… This is apparently caused by ServerStateItem in server_state_item.py line 43-48:

def _on_set(self):
        new_value_hash = calc_hash(self._value)
        if self._value_hash is None or self._value_hash != new_value_hash:
            self._rerun_bound_sessions()

        self._value_hash = new_value_hash

This is what causes update inside object doesn’t trigger a rerun… This behavior may be fixed through a proxy to the server_state objects as you mentioned. Case closed for this.

@TeddyHuang-00
Thank you for the investigation and the report.

Ah, I didn’t expect such usage where an object is used as a data container and set to a server-state item.

I think the current mechanism is necessary in some aspect, for example, in order to avoid infinite loops of re-running or to reduce unnecessary re-runs for efficiency sake.
FYI, as an excuse, this hashing mechanism is based on the implementation of value change detector in PySnooper. Additionally, I think this specific case is solved if object.__dict__ is added to the hashing targets.

As a general solution (or a workaround), what if a new function is introduced to forcefully rerun the sessions, something like streamtlit_server_state.force_rerun_bound_sessions()?

Thanks for your reply. I got the point now, and I agree to keep it unchanged. I’ll try to use the dict magic method to get around this problem.

And YES it would be very nice if we can force a rerun in other bound sessions, as it will avoid implicit logic.

@TeddyHuang-00 Thank you for your opinion.

I’ll try to use the __dict__ magic method to get around this problem.

Sorry for my ambiguity, but I meant that this specific problem can be solved by fixing the server-state on my side in the future release.

To avoid the problem with the current implementation, using something like dict, tuple, or namedtuple is a nice option. I personally recommend namedtuple for such data-container usage.

Thanks a lot for your explanation here! Sorry that I’m not that familiar with basics of Python and got confused. Thanks again for the namedtuple suggestion, but still I think it is nice and important to properly handdle objects in server state that might be important in other cases of usage. Really appreciate this great package!

@TeddyHuang-00 Hi, thank you.
I released a new version of streamlit-server-state, v0.8.0 with a new force_rerun_bound_sessions() method for this purpose.

Here is a sample based on the counter example (app_global_count.py) customized to use an object as a data container so that force_rerun_bound_sessions() is necessary to rerun the sessions.

import streamlit as st

from streamlit_server_state import server_state, server_state_lock, force_rerun_bound_sessions


class DataContainer:
    count = 0


with server_state_lock["data"]:  # Lock the "data" state for thread-safety
    if "data" not in server_state:
        server_state.data = DataContainer()

increment = st.button("Increment")
if increment:
    with server_state_lock.data:
        server_state.data.count += 1
        force_rerun_bound_sessions("data")

decrement = st.button("Decrement")
if decrement:
    with server_state_lock.data:
        server_state.data.count -= 1
        force_rerun_bound_sessions("data")

st.write("Count = ", server_state.data.count)

1 Like

Great to hear that! I didn’t got time to test it out though, I might read your code and try to figure out if this works for nested iterables and objects as well. Thanks again!


Yeah I have a quick peek on the code, and this may not be achievable. I think it’s just that the use case is too tricky, and the current library is good enough to cover most proper usage. Can’t see other problem gere!

This is a very nice component, as usual, great work @whitphx. Thanks for sharing! :raised_hands:

@Probability Thank you! I’m so glad.

How do you clear a server_state object?

I suppose developers reset (del or assign null values to) the fields manually when necessary.
Do you have some cases where it does not work?

del doesn’t work. Says key error.

It’s not overly important to delete I guess, I could just change fields