Skip to content

Commit 9caff06

Browse files
authored
Auto-release acquired stories on Disconnect (#642)
1 parent cada12f commit 9caff06

7 files changed

Lines changed: 139 additions & 13 deletions

File tree

client/cpp/include/chronolog_client.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,9 @@ class ChronologClientImpl;
111111

112112
// Top-level Chronolog Client. Implementation details in ChronologClientImpl.
113113
//
114-
// NOTE: ReleaseStory() must be called for all acquired stories before Disconnect();
115-
// otherwise Disconnect() returns CL_ERR_ACQUIRED (-4) and the client record is not removed.
116-
// TODO: Visor should auto-release acquired stories on Disconnect() — removing this requirement.
117-
// The destructor calls Disconnect() as best-effort; the Client is always safe to delete.
114+
// Disconnect() auto-releases any stories still acquired by the client so the
115+
// caller never has to walk its own acquisitions to clean up. The destructor
116+
// calls Disconnect() as best-effort; the Client is always safe to delete.
118117
class Client
119118
{
120119
public:

src/chrono-visor/include/ChronicleMetaDirectory.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ class ChronicleMetaDirectory
4343
int release_story(chronolog::ClientId const& client_id,
4444
const std::string& chronicle_name,
4545
const std::string& story_name,
46-
StoryId&);
46+
StoryId&,
47+
bool& was_last_acquirer);
48+
49+
// Release every story currently acquired by client_id. Returns the StoryIds
50+
// of stories whose last acquirer was this client, so the caller can notify
51+
// the recording groups to stop only those.
52+
int release_all_acquired_stories(chronolog::ClientId const& client_id,
53+
std::vector<StoryId>& released_with_no_acquirers_left);
4754

4855
int get_chronicle_attr(std::string const& name, const std::string& key, std::string& value);
4956

src/chrono-visor/include/ClientRegistryManager.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class ClientRegistryManager
3131

3232
ClientInfo* get_client_info(chronolog::ClientId const& client_id);
3333

34+
int get_acquired_stories_snapshot(chronolog::ClientId const& client_id,
35+
std::vector<std::pair<uint64_t, Story*>>& snapshot);
36+
3437
int add_story_acquisition(chronolog::ClientId const& client_id, uint64_t& sid, Story* pStory);
3538

3639
int remove_story_acquisition(chronolog::ClientId const& client_id, uint64_t& sid);

src/chrono-visor/src/ChronicleMetaDirectory.cpp

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,13 @@ int ChronicleMetaDirectory::acquire_story(chl::ClientId const& client_id,
262262
* chronolog::CL_ERR_NOT_EXIST if the Chronicle does not exist \n
263263
* chronolog::CL_ERR_UNKNOWN otherwise
264264
*/
265-
//TO_DO return acquisition_count after the story has been released
266265
int ChronicleMetaDirectory::release_story(chl::ClientId const& client_id,
267266
const std::string& chronicle_name,
268267
const std::string& story_name,
269-
StoryId& story_id)
268+
StoryId& story_id,
269+
bool& was_last_acquirer)
270270
{
271+
was_last_acquirer = false;
271272
LOG_DEBUG("[ChronicleMetaDirectory] ClientID={} releasing StoryName={} in ChronicleName={}",
272273
client_id,
273274
story_name.c_str(),
@@ -294,8 +295,11 @@ int ChronicleMetaDirectory::release_story(chl::ClientId const& client_id,
294295
if(acquirerMapRecord != acquirerMap.end())
295296
{
296297
/* All checks passed and entry found, manipulate metadata */
297-
/* Decrement AcquisitionCount */
298-
pStory->decrementAcquisitionCount();
298+
/* Decrement AcquisitionCount; the post-decrement value tells the caller
299+
* whether other clients still hold the story. The recording group must
300+
* only be told to stop when the last acquirer releases. */
301+
uint64_t remaining_acquirers = pStory->decrementAcquisitionCount();
302+
was_last_acquirer = (remaining_acquirers == 0);
299303
story_id = pStory->getSid();
300304
/* Remove this client from acquirerClientList of the Story */
301305
pStory->removeAcquirerClient(client_id);
@@ -317,6 +321,76 @@ int ChronicleMetaDirectory::release_story(chl::ClientId const& client_id,
317321
return ret;
318322
}
319323

324+
int ChronicleMetaDirectory::release_all_acquired_stories(chl::ClientId const& client_id,
325+
std::vector<StoryId>& released_with_no_acquirers_left)
326+
{
327+
released_with_no_acquirers_left.clear();
328+
if(clientRegistryManager_ == nullptr)
329+
{
330+
return chronolog::CL_ERR_UNKNOWN;
331+
}
332+
333+
// Snapshot under the ClientRegistryManager's mutex; release_story below
334+
// mutates acquiredStoryList_ via the manager and an unsynchronized walk
335+
// here would race with concurrent Acquire/Release on this client.
336+
std::vector<std::pair<uint64_t, Story*>> snapshot;
337+
int snapshot_ret = clientRegistryManager_->get_acquired_stories_snapshot(client_id, snapshot);
338+
if(snapshot_ret == chronolog::CL_ERR_NOT_EXIST)
339+
{
340+
// Nothing to release if the client has no record (e.g. already disconnected).
341+
return chronolog::CL_SUCCESS;
342+
}
343+
if(snapshot_ret != chronolog::CL_SUCCESS)
344+
{
345+
return snapshot_ret;
346+
}
347+
released_with_no_acquirers_left.reserve(snapshot.size());
348+
349+
for(auto& [sid, pStory]: snapshot)
350+
{
351+
if(pStory == nullptr)
352+
{
353+
continue;
354+
}
355+
356+
uint64_t cid = pStory->getCid();
357+
std::string const story_name = pStory->getName();
358+
359+
std::string chronicle_name;
360+
{
361+
std::lock_guard<std::mutex> chronicleMapLock(g_chronicleMetaDirectoryMutex_);
362+
auto chronicleRecord = chronicleMap_->find(cid);
363+
if(chronicleRecord == chronicleMap_->end())
364+
{
365+
continue;
366+
}
367+
chronicle_name = chronicleRecord->second->getName();
368+
}
369+
370+
StoryId released_id{0};
371+
bool was_last_acquirer = false;
372+
int ret = release_story(client_id, chronicle_name, story_name, released_id, was_last_acquirer);
373+
if(ret == chronolog::CL_SUCCESS)
374+
{
375+
// Only report ids whose last acquirer was this client. If other
376+
// clients still hold the story, the recording group must keep going.
377+
if(was_last_acquirer)
378+
{
379+
released_with_no_acquirers_left.push_back(released_id);
380+
}
381+
}
382+
else
383+
{
384+
LOG_WARNING("[ChronicleMetaDirectory] Failed to auto-release StoryName={} for ClientID={}: rc={}",
385+
story_name.c_str(),
386+
client_id,
387+
ret);
388+
}
389+
}
390+
391+
return chronolog::CL_SUCCESS;
392+
}
393+
320394
int ChronicleMetaDirectory::get_chronicle_attr(std::string const& name, const std::string& key, std::string& value)
321395
{
322396
LOG_DEBUG("[ChronicleMetaDirectory] Getting attributes Key={} from ChronicleName={}", key.c_str(), name.c_str());

src/chrono-visor/src/ClientRegistryManager.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ ClientInfo* ClientRegistryManager::get_client_info(chl::ClientId const& client_i
4747
}
4848
//////////////////////
4949

50+
int ClientRegistryManager::get_acquired_stories_snapshot(chl::ClientId const& client_id,
51+
std::vector<std::pair<uint64_t, Story*>>& snapshot)
52+
{
53+
snapshot.clear();
54+
std::lock_guard<std::mutex> clientRegistryLock(g_clientRegistryMutex_);
55+
auto clientRegistryRecord = clientRegistry_->find(client_id);
56+
if(clientRegistryRecord == clientRegistry_->end())
57+
{
58+
return chronolog::CL_ERR_NOT_EXIST;
59+
}
60+
auto const& acquiredStoryList = clientRegistryRecord->second.acquiredStoryList_;
61+
snapshot.reserve(acquiredStoryList.size());
62+
snapshot.assign(acquiredStoryList.begin(), acquiredStoryList.end());
63+
return chronolog::CL_SUCCESS;
64+
}
65+
//////////////////////
66+
5067
int ClientRegistryManager::add_story_acquisition(chl::ClientId const& client_id, uint64_t& sid, Story* pStory)
5168
{
5269
std::lock_guard<std::mutex> clientRegistryLock(g_clientRegistryMutex_);

src/chrono-visor/src/VisorClientPortal.cpp

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,25 @@ int chronolog::VisorClientPortal::ClientConnect(uint32_t client_euid,
137137
int chronolog::VisorClientPortal::ClientDisconnect(chronolog::ClientId const& client_id)
138138
{
139139
LOG_INFO("Client Disconnected. ClientID={}", client_id);
140+
141+
// Auto-release any stories the client is still holding so the client
142+
// record can always be cleaned up. Without this, remove_client_record
143+
// refuses to remove a client that has acquired stories and the client
144+
// would have to call ReleaseStory for every acquisition before Disconnect.
145+
//
146+
// release_all_acquired_stories reports back only the stories whose last
147+
// acquirer was this client; stories still held by other clients keep
148+
// recording.
149+
std::vector<StoryId> released_with_no_acquirers_left;
150+
chronicleMetaDirectory.release_all_acquired_stories(client_id, released_with_no_acquirers_left);
151+
if(theKeeperRegistry != nullptr)
152+
{
153+
for(StoryId const& released_id: released_with_no_acquirers_left)
154+
{
155+
theKeeperRegistry->notifyRecordingGroupOfStoryRecordingStop(released_id);
156+
}
157+
}
158+
140159
return clientManager.remove_client_record(client_id);
141160
}
142161

@@ -271,7 +290,8 @@ chl::AcquireStoryResponseMsg chronolog::VisorClientPortal::AcquireStory(chl::Cli
271290
player))
272291
{
273292
// RPC notification to the keepers might have failed, release the newly acquired story
274-
chronicleMetaDirectory.release_story(client_id, chronicle_name, story_name, story_id);
293+
bool was_last_acquirer = false;
294+
chronicleMetaDirectory.release_story(client_id, chronicle_name, story_name, story_id, was_last_acquirer);
275295
//we do know that there's no need notify keepers of the story ending in this case as it hasn't started...
276296
return chronolog::AcquireStoryResponseMsg(chronolog::CL_ERR_NO_KEEPERS, story_id, empty_keeper_service_ids);
277297
}
@@ -304,13 +324,19 @@ int chronolog::VisorClientPortal::ReleaseStory(chl::ClientId const& client_id,
304324
}
305325

306326
StoryId story_id(0);
307-
auto return_code = chronicleMetaDirectory.release_story(client_id, chronicle_name, story_name, story_id);
327+
bool was_last_acquirer = false;
328+
auto return_code =
329+
chronicleMetaDirectory.release_story(client_id, chronicle_name, story_name, story_id, was_last_acquirer);
308330
if(chronolog::CL_SUCCESS != return_code)
309331
{
310332
return return_code;
311333
}
312334

313-
theKeeperRegistry->notifyRecordingGroupOfStoryRecordingStop(story_id);
335+
// Only stop the recording group if no other client still holds the story.
336+
if(was_last_acquirer)
337+
{
338+
theKeeperRegistry->notifyRecordingGroupOfStoryRecordingStop(story_id);
339+
}
314340

315341
return chronolog::CL_SUCCESS;
316342
}

tests/integration/client/client_metadata_rpc_test.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ int main(int argc, char** argv)
130130
}
131131

132132
ret = client.Disconnect();
133-
assert(ret == chronolog::CL_ERR_NO_KEEPERS || ret == chronolog::CL_ERR_ACQUIRED);
133+
assert(ret == chronolog::CL_SUCCESS || ret == chronolog::CL_ERR_NO_KEEPERS);
134134

135135
t1 = std::chrono::steady_clock::now();
136136
std::vector<std::string> stories_names_retrieved;

0 commit comments

Comments
 (0)