Working with group views

Group views are the core data structure in Flock, representing the current (or desired, in the case of bootstrapping) membership and metadata of a group. This tutorial covers how to create, manipulate, and use group views.

What is a group view?

A group view contains:

Members: A list of group members, each with:

  • Network address (string)

  • Provider ID (uint16_t)

Metadata: Optional key-value pairs for application-specific information

Digest: A version identifier that changes when the view is modified

Example: Working with a group view

Here’s a complete example showing how to retrieve and work with a group view:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <margo.h>
#include <flock/flock-client.h>
#include <flock/flock-group.h>

int main(int argc, char** argv)
{
    if(argc != 3) {
        fprintf(stderr, "Usage: %s <server_address> <provider_id>\n", argv[0]);
        return -1;
    }

    const char* server_addr_str = argv[1];
    uint16_t provider_id = atoi(argv[2]);

    // Initialize Margo
    margo_instance_id mid = margo_init("na+sm", MARGO_CLIENT_MODE, 0, 0);
    assert(mid);

    // Initialize Flock client
    flock_client_t client;
    int ret = flock_client_init(mid, ABT_POOL_NULL, &client);
    assert(ret == FLOCK_SUCCESS);

    // Lookup server address
    hg_addr_t server_addr;
    ret = margo_addr_lookup(mid, server_addr_str, &server_addr);
    if(ret != HG_SUCCESS) {
        fprintf(stderr, "Failed to lookup server address\n");
        flock_client_finalize(client);
        margo_finalize(mid);
        return -1;
    }

    // Create group handle
    flock_group_handle_t group;
    ret = flock_group_handle_create(client, server_addr, provider_id, 0, &group);
    if(ret != FLOCK_SUCCESS) {
        fprintf(stderr, "Failed to create group handle\n");
        margo_addr_free(mid, server_addr);
        flock_client_finalize(client);
        margo_finalize(mid);
        return -1;
    }

    printf("Connected to Flock group\n");

    // Get current group view
    flock_group_view_t view = FLOCK_GROUP_VIEW_INITIALIZER;
    ret = flock_group_get_view(group, &view);
    assert(ret == FLOCK_SUCCESS);

    printf("\n=== Current Group View ===\n");
    printf("Group size: %zu members\n", view.members.size);
    printf("View digest: %lu\n", (unsigned long)view.digest);

    // Print metadata if any
    if(view.metadata.size > 0) {
        printf("Metadata (%zu entries):\n", view.metadata.size);
        for(size_t i = 0; i < view.metadata.size; i++) {
            printf("  %s = %s\n",
                   view.metadata.data[i].key,
                   view.metadata.data[i].value);
        }
    }

    // Print all members
    printf("\nGroup members:\n");
    for(size_t i = 0; i < view.members.size; i++) {
        printf("  [%zu] Address: %s\n", i, view.members.data[i].address);
        printf("      Provider ID: %u\n", view.members.data[i].provider_id);
    }

    // Get number of members using the view helper function
    size_t num_members = flock_group_view_member_count(&view);
    printf("\nNumber of members (via helper): %zu\n", num_members);

    // Clean up view
    flock_group_view_clear(&view);

    // Release group handle
    flock_group_handle_release(group);

    // Free server address
    margo_addr_free(mid, server_addr);

    // Finalize client
    flock_client_finalize(client);

    // Finalize Margo
    margo_finalize(mid);

    printf("\nGroup view operations completed successfully\n");

    return 0;
}

Initialization

Always initialize views before use:

flock_group_view_t view = FLOCK_GROUP_VIEW_INITIALIZER;

Initialization from a Margo instance (self):

flock_group_view_init_from_self(mid, provider_id, &view);

Initialization from MPI:

flock_group_view_init_from_mpi(mid, provider_id, MPI_COMM_WORLD, &view);

Adding/removing members

Add a single member:

flock_group_view_add_member(&view, "na+sm://12345-0", 42);
flock_group_view_add_member(&view, "na+sm://12345-1", 42);

This function returns the flock_member_t* pointer to the newly created member structure in the view. This pointer can be used to e.g. manipulate its extra field and attach data to the member. This pointer is also what will be needed to remove a member.

Removing a member:

flock_group_view_remove_member(&view, member);

Accessing members

Get the number of members:

size_t count = flock_group_view_member_count(&view);

Get a member by index:

flock_member_t* member = flock_group_view_member_at(&view, 0);
if(member) {
    printf("Address: %s, Provider ID: %u\n",
           member->address, member->provider_id);
}

Find a member by address and provider ID:

flock_member_t* member = flock_group_view_find_member(
    &view, "na+sm://12345-0", 42);
if(member) {
    // Member found
}

Compare two members:

int cmp = flock_member_cmp(member1, member2);
// Returns 0 if equal, -1 if member1 < member2, 1 if member1 > member2

Iterate over all members:

for(size_t i = 0; i < flock_group_view_member_count(&view); i++) {
    flock_member_t* m = flock_group_view_member_at(&view, i);
    printf("Member %zu: %s (provider %u)\n",
           i, m->address, m->provider_id);
}

Adding metadata

Metadata is optional but can be useful for:

  • Service identification

  • Version information

  • Configuration data

  • Custom application data

Add metadata:

flock_group_view_add_metadata(&view, "service", "my_service");
flock_group_view_add_metadata(&view, "version", "1.0.0");
flock_group_view_add_metadata(&view, "region", "us-west");

Access metadata:

for(size_t i = 0; i < view.metadata.size; i++) {
    printf("%s = %s\n",
           view.metadata.data[i].key,
           view.metadata.data[i].value);
}

Find metadata by key:

const char* value = flock_group_view_find_metadata(&view, "version");
if(value) {
    printf("Version: %s\n", value);
}

Remove metadata by key:

bool removed = flock_group_view_remove_metadata(&view, "region");
// Returns true if metadata was removed, false if key wasn't found

Get the number of metadata entries:

size_t count = flock_group_view_metadata_count(&view);

Get metadata by index:

flock_metadata_t* meta = flock_group_view_metadata_at(&view, 0);
if(meta) {
    printf("Key: %s, Value: %s\n", meta->key, meta->value);
}

Iterate over all metadata:

for(size_t i = 0; i < flock_group_view_metadata_count(&view); i++) {
    flock_metadata_t* m = flock_group_view_metadata_at(&view, i);
    printf("%s = %s\n", m->key, m->value);
}

Serialization

Serialize to a file:

flock_return_t ret = flock_group_view_serialize_to_file(&view, "mygroup.flock");
if(ret != FLOCK_SUCCESS) {
    // Handle error
}

Deserialize from a file:

flock_group_view_t view = FLOCK_GROUP_VIEW_INITIALIZER;
flock_return_t ret = flock_group_view_from_file("mygroup.flock", &view);
if(ret != FLOCK_SUCCESS) {
    // Handle error
}

Deserialize from a string:

const char* json_str = "{...}";  // JSON representation of the view
flock_return_t ret = flock_group_view_from_string(json_str, strlen(json_str), &view);

Serialize with a custom callback:

void my_serializer(void* ctx, const char* data, size_t len) {
    // Custom serialization logic, e.g. write to a buffer
}

flock_group_view_serialize(&view, my_serializer, my_context);

Cleaning up

Free view resources:

flock_group_view_clear(&view);

This frees all internal allocations (member addresses, metadata, etc.). Always call this when done with a view.

View digest

The digest is a hash of the view contents, useful for:

  • Quick comparison

  • Version tracking

  • Change detection

Get the digest:

uint64_t digest = flock_group_view_digest(&view);

The digest is automatically updated when you modify the view through the API.

Moving views

The FLOCK_GROUP_VIEW_MOVE macro efficiently moves the content of one view into another without copying data:

flock_group_view_t src = FLOCK_GROUP_VIEW_INITIALIZER;
flock_group_view_t dst = FLOCK_GROUP_VIEW_INITIALIZER;

// ... populate src ...

FLOCK_GROUP_VIEW_MOVE(&src, &dst);
// src is now empty, dst contains the data

Warning: The destination must be empty (or already cleared) before the move, otherwise it will cause a memory leak.

Thread safety

Group views include a mutex for thread-safe access. Use the provided macros when accessing or modifying views from multiple threads:

FLOCK_GROUP_VIEW_LOCK(&view);
// Access or modify the view safely
size_t count = flock_group_view_member_count(&view);
FLOCK_GROUP_VIEW_UNLOCK(&view);

Note: The individual functions (flock_group_view_add_member, etc.) do not acquire the lock themselves. It is the caller’s responsibility to lock the view when concurrent access is possible.

Extra data

Each member has an extra field that backends can use to attach custom data:

flock_member_t* member = flock_group_view_add_member(&view, address, id);
member->extra.data = my_custom_data;
member->extra.free = my_free_function;  // Called when member is removed

Clear all extra data:

flock_group_view_clear_extra(&view);

This calls the free function for each member’s extra data and sets the pointers to NULL, but does not remove the members from the view.

Best practices

1. Always initialize:

flock_group_view_t view = FLOCK_GROUP_VIEW_INITIALIZER;

2. Always clean up:

flock_group_view_clear(&view);

3. Use the API, don’t manipulate directly:

// Good
flock_group_view_add_member(&view, address, provider_id);

// Bad
view.members.data[i].address = strdup(address);  // Don't do this!

4. Check return values:

// Functions returning pointers: check for NULL
flock_member_t* member = flock_group_view_add_member(&view, address, provider_id);
if(!member) {
    // Handle allocation error
}

// Functions returning flock_return_t: check for FLOCK_SUCCESS
flock_return_t ret = flock_group_view_serialize_to_file(&view, "mygroup.flock");
if(ret != FLOCK_SUCCESS) {
    // Handle error
}

5. Lock when accessing from multiple threads:

FLOCK_GROUP_VIEW_LOCK(&view);
// ... access or modify ...
FLOCK_GROUP_VIEW_UNLOCK(&view);