Skip to content

[WIP] Avatar persistence and synchronization#248

Open
jolavillette wants to merge 8 commits intoRetroShare:masterfrom
jolavillette:AvatarStorageAndUpdateImprove
Open

[WIP] Avatar persistence and synchronization#248
jolavillette wants to merge 8 commits intoRetroShare:masterfrom
jolavillette:AvatarStorageAndUpdateImprove

Conversation

@jolavillette
Copy link
Contributor

Code and comment by Gemini

This summary describes the technical implementation of the new avatar persistence and synchronization system for RetroShare's Chat Service.

Core Implementation Overview
The goal of these changes was to provide a reliable, persistent, and proactive avatar exchange mechanism that operates silently in the background without interrupting the user experience or breaking existing services like Chat Lobbies.

  1. Persistence and Data Integrity
    To ensure avatars survive application restarts, the storage logic was migrated to a Key-Value set system within the configuration.

saveList: Now encodes the local node's avatar and all cached peer avatars into RsConfigKeyValueSet items using Radix64 encoding. It explicitly calls parent services to ensure Chat Lobby subscriptions are also saved.

loadList: Implements a non-destructive "Pass-through" logic. It extracts avatar data from KV sets but relays any unrecognized items to DistributedChatService::processLoadListItem and DistantChatService::processLoadListItem.

Lobby Protection: This delegation ensures that shared configuration containers containing lobby data or GXS identities are not deleted by the Chat Service during the loading phase.

  1. Real-time Synchronization & Handshaking
    The system now proactively ensures that avatars are up-to-date across the network through two main triggers:

Broadcast on Update (setOwnNodeAvatarData): When the user changes their avatar, the service immediately broadcasts the new image data to all currently online peers.

Handshake on Connection (statusChange): As soon as a peer connects, a bidirectional handshake is initiated. The service sends an avatarRequest to the peer and simultaneously pushes the local avatar data to them.

Silent Protocol: These exchanges use direct RsChatAvatarItem packets, which are processed by the remote UI in the background, preventing unnecessary chat window popups.

  1. Thread Safety and Stability
    A critical deadlock identified through GDB debugging was resolved to ensure system stability.

Deadlock Prevention: The RsStackMutex was removed from makeOwnAvatarItem to prevent recursive locking when called from parent functions that already hold the mChatMtx lock.

Scoped Locking: In setOwnNodeAvatarData, the mutex is now confined to a specific scope to ensure it is released before entering the network broadcast loop, allowing child functions to acquire the lock safely.

@jolavillette jolavillette force-pushed the AvatarStorageAndUpdateImprove branch from 93fe7c5 to f7273a3 Compare January 24, 2026 07:58

mChatMtx.lock(); /****** MUTEX LOCKED *******/
/* 2. Original developer items (Messages & Status) */
mChatMtx.lock();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like these .lock() (I know it was here before). It's probably a good opportunity to replace it with a stack mutex.

ci->PeerId(mServiceCtrl->getOwnId());
RsConfigKeyValueSet *item = new RsConfigKeyValueSet();
RsTlvKeyValue kv; kv.key = "AV_CACHE:OWN";
Radix64::encode(_own_avatar->_image_data, _own_avatar->_image_size, kv.value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: RsConfigKeyValueSet is rather limiting in the type of what would be used (basically strings), which explains the Radix64 format. You would get a smaller print using an explicit encoding in the RsItem, but this will cause problems of backward compatibility.

RsChatAvatarItem *p3ChatService::makeOwnAvatarItem()
{
RsStackMutex stack(mChatMtx); /********** STACK LOCKED MTX ******/
/* Mutex is removed here to prevent recursive deadlock from parent calls */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good practice: if you remove the mutex, rename the method as locked_makeOwnAvatarItem() : this indicates that the method should be called within a mutex-protected region.

RsDbg() << "AVATAR entering loadList, total items: " << (int)load.size();

for(std::list<RsItem*>::const_iterator it(load.begin());it!=load.end();++it)
for(std::list<RsItem*>::iterator it(load.begin()); it != load.end(); )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you changed the way your own avatar is saved, this is not backward compatible: patched users will lose their avatar and will have to re-define it.

I propose a better alternative: use RsChatAvatarItem as before, setting the PeerId() of the item to the node ID of that avatar. This is backward compatible with what's done now, and also will require less storage since it's not converting to Radix64.

RsChatStatusItem *mitem = dynamic_cast<RsChatStatusItem *>(item);
if(mitem)
{
_custom_status_string = mitem->status_string ;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove the mutex?

@csoler
Copy link
Contributor

csoler commented Jan 24, 2026

Use RsChatAvatarItem instead of KeyValueSet => it will be backward compatible and the code is also simpler.

@jolavillette
Copy link
Contributor Author

Following Cyril comments:

  • Own avatar is saved as a RsChatAvatarItem (existing logic confirmed/preserved), insuring backward compatibility. Saved ID is 0, and is interpreted as our ID, see below.
  • For peers the legacy RsChatAvatarItem could not be used because its serialization ignores the Peer ID (saving only image data), causing lost IDs (zero) on reload. Implemented custom saving using RsConfigKeyValueSet to persist both Peer ID and Avatar Data. Avatars are saved in chunks (10 items per key-value set) to respect serializer size limits and ensure reliable writing to disk.
  • Various code improvements

@jolavillette jolavillette force-pushed the AvatarStorageAndUpdateImprove branch 2 times, most recently from ba0143c to 9a9813e Compare January 27, 2026 06:29
}
_avatars[ci->PeerId()] = new AvatarInfo(ci->image_data,ci->image_size) ;
_avatars[ci->PeerId()]->_peer_is_new = true ;
_avatars[ci->PeerId()]->_own_is_new = new_peer ;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you keep track of peers to which we haven't sent our own avatar yet?

uint32_t s = 0 ;
#ifdef CHAT_DEBUG
std::cerr << "p3chatservice:: own avatar requested from above. " << std::endl ;
//std::cerr << "p3chatservice:: own avatar requested from above. " << std::endl ;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you left CHAT_DEBUG on. Make sure all debug output is disabled/cleaned before we merge.

RsPeerId pid = ai->PeerId();
if(pid.isNull() || pid == mServiceCtrl->getOwnId()) {
if(_own_avatar == NULL) _own_avatar = new AvatarInfo(ai->image_data, ai->image_size);
item_handled = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the need for this item_handled mechanism. In the end, all items that have been loaded need to be destroyed (unless one was actually stored. Does this happen? We didn't have this check before).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see. Each step would delete the item separately. So it's fine as it is now.

}

if(kv != NULL) {
if(!kv->tlvkvs.pairs.empty())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check is not necessary (following the code above it's not possible to have an item with an empty list). It's of course not a problem to keep it.


/* AVATAR Handshake on connection */
RsDbg() << "AVATAR peer connected, initiating sync with: " << it->id.toStdString().c_str();
sendAvatarRequest(it->id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to cause sending the avatar at every connection. To make it reliable we should send the TS of the avatar we have, and if the peer has a more recent avatar then it should send it. Looking at the code in sendAvatarRequest() it should only be used if we do nto have the avatar.

sendAvatarRequest(it->id);
if(_own_avatar != nullptr && _own_avatar->_image_size > 0)
{
sendAvatarJpegData(it->id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. Why would we send our own avatar without a request?

@jolavillette
Copy link
Contributor Author

  1. New packet type: Added RsChatAvatarInfoItem containing only the avatar timestamp (replaces systematic full image broadcasting)
  2. Connection handshake: On peer connection, we now send [sendAvatarInfo()] (timestamp only) instead of [sendAvatarJpegData()] (full image)
  3. Smart pull mechanism:
    • Peer receives timestamp and compares with local cache
    • If remote timestamp is newer → peer sends [sendAvatarRequest()]
    • If timestamp is same/older → no action (bandwidth saved)
  4. Avatar updates: When user changes their avatar, the new timestamp is broadcast to all connected peers, triggering the same pull mechanism
  5. Debug cleanup: Disabled CHAT_DEBUG define (debug blocks kept for future debugging)

@jolavillette jolavillette force-pushed the AvatarStorageAndUpdateImprove branch from 0242ad7 to 5bece81 Compare January 27, 2026 21:21
@jolavillette
Copy link
Contributor Author

Save/restore of avatars is broken since last commit (TS). Stand by for a fix.

…t missing avatar on peer connection for backward compatibility
@jolavillette
Copy link
Contributor Author

Fix Avatar Persistence Regression

Fixes avatar persistence issue introduced by timestamp sync implementation.

Problem: Avatars were not persisting across sessions because _timestamp was not updated when receiving avatar data.

Solution:

  • Update timestamp in AvatarInfo::init() when image data is set
  • Request peer avatars on connection if not cached (backward compatibility)

Result: Avatars now persist correctly and work with both old and new nodes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants