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 ofalaner_idorexternal_user_idmust 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:
- Collects all direct
SmartGroupDefinitionsubclasses - Collects all
SmartGroupGeneratorsubclasses, callssmart_groups()on each - 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 | 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]
Per-group sync details¶
- Find or create the
SmartGrouprow byslugify(name) - Update metadata: name, description, handles,
github_url, storedmembers_querysource - Compute target members via
active_members()(filters out administratively suspended alaners) - Diff memberships:
- New members: bulk insert with
start_date=today - Removed members: set
end_date=yesterday(oris_cancelled=Trueif started today)
- New members: bulk insert with
- Same process for external users
- Sanity check: calls
sanity_check()- exception rolls back this group only - 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
SmartGroupMembershiprecords allow querying "who was in group X on date Y" viais_active_on(date) - Current state: the
SmartGroup.membersrelationship filters onis_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_idFK toSmartGroup. UsesSmartGroupMembership.is_active_on(start_date)for historical snapshots of who was reviewable. - Oncall groups:
OncallGroup.roster_smart_group_idcan reference a smart group as the roster source. TheOncallStaticRostersgenerator creates fallback groups for oncall groups without a custom roster. - Role grant rules: rules like
CommunitySecurityReferentsBaselineRoleGrantRuleuse smart group membership to determine who gets specific roles. - Ownership: smart groups can be linked to ownership records (e.g.,
CrewEngineerscreates dual ownership linking the smart group to its crew).
Adding a new smart group¶
Static group¶
- Create a subclass of
SmartGroupDefinitionin the appropriate module undercommands/smart_groups/ - Set
name,description, and any integration handles (slack_handle,github_handle, etc.) - Implement
members_on(date)(ormembers()for legacy compat) returning aset[Alaner] - Optionally implement
sanity_check()to validate the membership - Ensure the module is imported in
scheduler_group.py
Dynamic group family¶
- Create a subclass of
SmartGroupGeneratorin the appropriate module - Implement
smart_groups()returning a collection ofBaseSmartGroupDefinitionsubclasses - Each generated class needs
name,description, and amembers()ormembers_on()implementation - Ensure the module is imported in
scheduler_group.py