Track user activity in a persistent cache
Add a new Groovy script `track-active-users-1.0.groovy` to track user
activities by entering their `id` and timestamp into
`track-active-users_cache` persistent cache.
The tracking is implemented as a side effect on a new
`GroupBackend` implementation that handles all group `UUID`'s. This way
our backend `membershipsOf(CurrentUser)` method will be called whenever
any group membership resolution is done. This gives us a single
place to get the user activities for all supported entry points (UI,
REST, SSH, git-over-http). The impact of on the user group resolution
should be neglectable and should be equal to the performance of the
persistent cache `get()` and `put()` calls. The user activity timestamp
is stored with one-minute precision.
Bear in mind, that in the case of git-over-http access to public
repositories, even when the user provides authentication details, Gerrit
will not consult any of the `GroupBackend`'s. This means that clones and
fetches of publicly visible repositories will not be counted as user
activity.
Bug: Issue Issue 327730871
Change-Id: I5827288d40bba4c6b1e7b20d9aae822db8eabc29
diff --git a/admin/README.md b/admin/README.md
index 7143588..9cca543 100644
--- a/admin/README.md
+++ b/admin/README.md
@@ -11,3 +11,4 @@
* [warm-cache-1.0.groovy](/admin/warm-cache-1.0.groovy) - Controls the Gerrit cache warm-up via command-line
* [readonly-1.0.groovy](/admin/readonly-1.0.groovy) - Set all Gerrit projects in read-only mode during maintenance
* [stale-packed-refs-1.0.groovy](/admin/stale-packed-refs-1.0.groovy) - Check all specified projects and expose metric with age of `packed-refs.lock` files
+* [track-active-users.groovy](/admin/track-active-users.groovy) - Tracks users login in `track-active-users_cache`
diff --git a/admin/track-active-users-1.0.groovy b/admin/track-active-users-1.0.groovy
new file mode 100644
index 0000000..86a77a8
--- /dev/null
+++ b/admin/track-active-users-1.0.groovy
@@ -0,0 +1,92 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import com.google.common.cache.*
+import com.google.gerrit.common.*
+import com.google.gerrit.entities.*
+import com.google.gerrit.extensions.registration.*
+import com.google.gerrit.server.*
+import com.google.gerrit.server.account.*
+import com.google.gerrit.server.cache.*
+import com.google.gerrit.server.project.*
+import com.google.inject.*
+import com.google.inject.name.*
+
+import java.time.*
+
+import static java.util.concurrent.TimeUnit.*
+
+class TrackActiveUsersCache extends CacheModule {
+ static final NAME = "users_cache"
+ static final DEFAULT_CACHE_TTL = Duration.ofDays(90)
+ static final MAX_SIZE = 300_000
+
+ @Override
+ protected void configure() {
+ persist(NAME, Integer, Long)
+ .diskLimit(MAX_SIZE)
+ .maximumWeight(MAX_SIZE)
+ .expireAfterWrite(DEFAULT_CACHE_TTL)
+ }
+}
+
+@Singleton
+class TrackingGroupBackend implements GroupBackend {
+ @Inject
+ @Named(TrackActiveUsersCache.NAME)
+ Cache<Integer, Long> trackActiveUsersCache
+
+ @Override
+ boolean handles(AccountGroup.UUID uuid) {
+ return true
+ }
+
+ @Override
+ GroupDescription.Basic get(AccountGroup.UUID uuid) {
+ return null
+ }
+
+ @Override
+ Collection<GroupReference> suggest(String name, @Nullable ProjectState project) {
+ return Collections.emptyList()
+ }
+
+ @Override
+ GroupMembership membershipsOf(CurrentUser user) {
+ if (user.identifiedUser) {
+ def accountId = user.accountId.get()
+ def currentMinutes = MILLISECONDS.toMinutes(System.currentTimeMillis())
+ if (trackActiveUsersCache.getIfPresent(accountId) != currentMinutes) {
+ trackActiveUsersCache.put(accountId, currentMinutes)
+ }
+ }
+ return GroupMembership.EMPTY
+ }
+
+ @Override
+ boolean isVisibleToAll(AccountGroup.UUID uuid) {
+ return false
+ }
+}
+
+class TrackActiveUsersModule extends AbstractModule {
+ @Override
+ void configure() {
+ install(new TrackActiveUsersCache())
+
+ DynamicSet.bind(binder(), GroupBackend).to(TrackingGroupBackend)
+ }
+}
+
+modules = [TrackActiveUsersModule]