Skip to content

Smart Groups

Overview

Smart Groups are declarative, code-defined groups whose membership is computed from database queries. They are:

  • Code-driven: each group is a Python class with a members_on(date) method
  • Synced hourly: a scheduled command recomputes memberships every hour (at :15)
  • Historized: membership changes are tracked with start_date/end_date, enabling time-travel queries
  • Externally synced: memberships propagate to GitHub teams, Google Groups, Slack usergroups, and Sentry
  • Oncall-ready: smart groups can serve as dynamic rosters for on-call groups via OncallGroup.roster_smart_group_id, replacing static roster management with code-defined membership

Data model

SmartGroup

Extends BaseAlanGroup (which provides name, description, slack_handle, github_handle, google_group, sync_external_users).

Field Type Description
smart_group_definition_id Text (unique) Slugified name, links DB row to Python class
github_parent_group_id Integer? Parent GitHub team ID
members_query Text? Stored source code of the members() method
github_url Text? Link to the definition source on GitHub
can_be_used_as_smart_roster Boolean Whether this group can serve as an oncall roster

Relationships: members (many-to-many with Alaner), external_users (many-to-many with ExternalUser), ownership (one-to-many with Ownership).

SmartGroupMembership

Extends HistorizableAlanerRelation (provides start_date, end_date, is_cancelled, alaner_id, is_active).

Field Type Description
smart_group_id UUID (FK) Reference to the parent SmartGroup
alaner_id Integer? XOR with external_user_id
external_user_id UUID? XOR with alaner_id

Constraints:

  • ExcludeConstraint: no overlapping active memberships per (alaner, smart_group)
  • ExcludeConstraint: no overlapping active memberships per (external_user, smart_group)
  • CheckConstraint: exactly one of alaner_id or external_user_id must be set

Definition framework

All definitions live under apps/eu_tools/alan_home/commands/smart_groups/.

BaseSmartGroupDefinition (abstract)

Class attributes configure the group's metadata and external integrations:

class BaseSmartGroupDefinition(ABC):
    name: str                              # human-readable name
    description: str                       # purpose description
    slack_handle: str | None = None        # Slack usergroup handle
    github_handle: str | None = None       # GitHub team slug
    github_parent_group_id: int | None = None
    google_group: str | None = None        # Google Group email prefix
    can_be_used_as_smart_roster: bool = True

Key methods:

Method Description
members_on(date) Returns set[Alaner] active on a given date. Override this.
external_users_on(date) Returns set[ExternalUser] on a given date
active_members() Calls members_on(today) filtered by not alaner.is_ended
active_external_users() Same, for external users
sanity_check() Override to validate; raise to abort the sync

SmartGroupDefinition (static single groups)

Subclass this to define a single, fixed group:

class AllAlaners(SmartGroupDefinition):
    name = "All Alaners"
    slack_handle = "alaners"
    github_handle = "alan-team"
    google_group = "team"

    @classmethod
    def members(cls) -> set[Alaner]:
        return set(
            current_session.query(Alaner).filter(
                Alaner.is_started_on(utctoday() + timedelta(days=14)),
                ~Alaner.is_ended,
            )
        )

    @classmethod
    def sanity_check(cls) -> None:
        if len(cls.members()) < 500:
            raise ValueError("Less than 500 members in the group")

SmartGroupGenerator (dynamic families)

Subclass this to generate a family of groups from database state. Implement smart_groups() returning a collection of BaseSmartGroupDefinition subclasses:

class CrewEngineers(SmartGroupGenerator):
    @classmethod
    def smart_groups(cls) -> Collection[type[BaseSmartGroupDefinition]]:
        crews = current_session.execute(
            select(Crew).filter(Crew.is_active, Crew.type == CrewType.product)
        ).scalars().all()
        return [make_crew_group(crew) for crew in crews]

Each generated class is a full BaseSmartGroupDefinition with its own name, members(), integration handles, etc.

Discovery

get_all_smart_group_definitions() builds the full registry:

  1. Collects all direct SmartGroupDefinition subclasses
  2. Collects all SmartGroupGenerator subclasses, calls smart_groups() on each
  3. Returns dict[slugified_name, definition_class]

All definition modules must be imported in scheduler_group.py so Python class introspection can discover them.

Examples

Static groups

Class Module Slack GitHub Google Description
AllAlaners people.py @alaners alan-team team@ All alaners (started within 14d, not ended)
AreaLeads company_org.py @area_leads - - All active area leads
EngOncallRoster rosters.py @eng-oncall-roster eng-oncall-roster - Engineering oncall-eligible members
ParisEngAlaners engineering.py @paris_eng_alaners - - Engineers in the Paris area
EngCoaches engineering.py @eng_coaches - - Coaches of Engineering community members

Generators

Class Module Pattern Description
CrewEngineers engineering.py {crew} crew engineers One group per active product crew, with custom ownership()
AreaDeputies company_org.py {area} area deputies One group per area with slack handle
CareByCountry care.py Care community for {country} One group per country
OncallStaticRosters rosters.py Roster of {oncall_group} One group per oncall group without a custom roster

Daily sync

The flask smart-groups update command runs every hour at :15 (monitored on #alan_home_alerts, failures only).

flowchart TD
    A[Start: flask smart-groups update] --> B{Canary: dry-run AllAlaners}
    B -->|Fail| C[Abort all updates]
    B -->|Pass| D[Discover all definitions]
    D --> E[For each definition]
    E --> F[Find or create SmartGroup row]
    F --> G[Update metadata & source code]
    G --> H[Compute active_members]
    H --> I[Diff current vs target]
    I --> J[Bulk insert new memberships]
    I --> K[End-date removed memberships]
    J --> L[Same for external users]
    K --> L
    L --> M[Run sanity_check]
    M --> N[Handle custom ownership]
    N --> O[Commit or rollback]
    O --> E
    E --> P[Cleanup stale groups]
    P --> Q[End]
Hold "Alt" / "Option" to enable pan & zoom

Per-group sync details

  1. Find or create the SmartGroup row by slugify(name)
  2. Update metadata: name, description, handles, github_url, stored members_query source
  3. Compute target members via active_members() (filters out administratively suspended alaners)
  4. Diff memberships:
    • New members: bulk insert with start_date=today
    • Removed members: set end_date=yesterday (or is_cancelled=True if started today)
  5. Same process for external users
  6. Sanity check: calls sanity_check() - exception rolls back this group only
  7. Custom ownership: if the definition has an ownership() method, create/update the ownership record

Stale group cleanup

After syncing all current definitions, groups whose smart_group_definition_id is no longer in the registry get their active memberships ended and end_date set to today.

Date-aware membership

  • Historical queries: the historized SmartGroupMembership records allow querying "who was in group X on date Y" via is_active_on(date)
  • Current state: the SmartGroup.members relationship filters on is_active (no end date, not cancelled)
  • Future dates: members_on(future_date) computes on the fly from the definition's query

External syncs

Each sync runs as a separate scheduled command (hourly on weekdays):

System Command Handle field Schedule
GitHub teams flask github sync_static_groups github_handle Hourly 5am-6pm weekdays
Google Groups flask google sync_static_groups google_group Hourly 5am-6pm weekdays
Slack usergroups flask slack2 sync_static_groups slack_handle Hourly 5am-6pm weekdays
Sentry teams flask sentry sync_static_groups github_handle (in CODEOWNERS) Hourly 8am-6pm weekdays

These commands sync all BaseAlanGroup subclasses (not just smart groups) that have the relevant handle set.

Integration with other systems

  • Review cycles: ReviewCycle.reviewees_smart_group_id FK to SmartGroup. Uses SmartGroupMembership.is_active_on(start_date) for historical snapshots of who was reviewable.
  • Oncall groups: OncallGroup.roster_smart_group_id can reference a smart group as the roster source. The OncallStaticRosters generator creates fallback groups for oncall groups without a custom roster.
  • Role grant rules: rules like CommunitySecurityReferentsBaselineRoleGrantRule use smart group membership to determine who gets specific roles.
  • Ownership: smart groups can be linked to ownership records (e.g., CrewEngineers creates dual ownership linking the smart group to its crew).

Adding a new smart group

Static group

  1. Create a subclass of SmartGroupDefinition in the appropriate module under commands/smart_groups/
  2. Set name, description, and any integration handles (slack_handle, github_handle, etc.)
  3. Implement members_on(date) (or members() for legacy compat) returning a set[Alaner]
  4. Optionally implement sanity_check() to validate the membership
  5. Ensure the module is imported in scheduler_group.py

Dynamic group family

  1. Create a subclass of SmartGroupGenerator in the appropriate module
  2. Implement smart_groups() returning a collection of BaseSmartGroupDefinition subclasses
  3. Each generated class needs name, description, and a members() or members_on() implementation
  4. Ensure the module is imported in scheduler_group.py