diff --git a/discovery-provider/src/api/v1/users.py b/discovery-provider/src/api/v1/users.py index e5291082c18..f21f9d1f8a2 100644 --- a/discovery-provider/src/api/v1/users.py +++ b/discovery-provider/src/api/v1/users.py @@ -828,7 +828,22 @@ def get(self, id): argument_class=DescriptiveArgument ) users_by_content_node_route_parser.add_argument( - "creator_node_endpoint", required=True, type=str + "creator_node_endpoint", + required=True, + type=str, + description="Get users who have this Content Node endpoint as their primary/secondary", +) +users_by_content_node_route_parser.add_argument( + "prev_user_id", + required=False, + type=int, + description="Minimum user_id to return. Used for pagination as the offset after sorting in ascending order by user_id", +) +users_by_content_node_route_parser.add_argument( + "max_users", + required=False, + type=int, + description="Maximum number of users to return (SQL LIMIT)", ) users_by_content_node_response = make_full_response( "users_by_content_node", full_ns, fields.List(fields.Nested(user_replica_set)) @@ -837,26 +852,39 @@ def get(self, id): @full_ns.route("/content_node/", doc=False) class UsersByContentNode(Resource): - @full_ns.marshal_with(users_by_content_node_response) - # @cache(ttl_sec=30) - def get(self, replica_type): - """New route to call get_users_cnode with replica_type param (only consumed by content node) - - Leaving `/users/creator_node` above untouched for backwards-compatibility - + @ns.doc( + id="""Get Users By Replica Type for Content Node""", + description=""" + (Only consumed by Content Node) Gets users that have a given Content Node endpoint as + their primary, secondary, or either (depending on the replica_type passed). Response = array of objects of schema { user_id, wallet, primary, secondary1, secondary2, primarySpId, secondary1SpID, secondary2SpID } - """ + """, + responses={200: "Success", 400: "Bad request", 500: "Server error"}, + ) + @full_ns.marshal_with(users_by_content_node_response) + @cache(ttl_sec=5 * 60) + def get(self, replica_type): args = users_by_content_node_route_parser.parse_args() + # Endpoint that a user's primary/secondary/either must be set to for them to be included in the results cnode_url = args.get("creator_node_endpoint") + # Used for pagination with ">" comparison in SQL query. See https://ivopereira.net/efficient-pagination-dont-use-offset-limit + prev_user_id = args.get("prev_user_id") + # LIMIT used in SQL query + max_users = args.get("max_users") if replica_type == "primary": - users = get_users_cnode(cnode_url, ReplicaType.PRIMARY) + users = get_users_cnode( + cnode_url, ReplicaType.PRIMARY, prev_user_id, max_users + ) elif replica_type == "secondary": - users = get_users_cnode(cnode_url, ReplicaType.SECONDARY) + users = get_users_cnode( + cnode_url, ReplicaType.SECONDARY, prev_user_id, max_users + ) else: - users = get_users_cnode(cnode_url, ReplicaType.ALL) + users = get_users_cnode(cnode_url, ReplicaType.ALL, prev_user_id, max_users) return success_response(users) diff --git a/discovery-provider/src/queries/get_users_cnode.py b/discovery-provider/src/queries/get_users_cnode.py index eb74f4a6951..a97bed0bea5 100644 --- a/discovery-provider/src/queries/get_users_cnode.py +++ b/discovery-provider/src/queries/get_users_cnode.py @@ -13,7 +13,13 @@ class ReplicaType(Enum): ALL = 3 -def get_users_cnode(cnode_endpoint_string, replica_type=ReplicaType.PRIMARY): +# TODO: Enforce default max_users after all CNs are updated to a version with this PR: https://github.com/AudiusProject/audius-protocol/pull/3064 +def get_users_cnode( + cnode_endpoint_string, + replica_type, + prev_user_id, + max_users, +): """ Query all users with `cnode_endpoint_string` in replica set If replica_type=ReplicaType.PRIMARY -> returns users with `cnode_endpoint_string` as primary @@ -67,11 +73,29 @@ def get_users_cnode(cnode_endpoint_string, replica_type=ReplicaType.PRIMARY): 't.secondary1 = :cnode_endpoint_string OR ' 't.secondary2 = :cnode_endpoint_string) AND' } - t.secondary1 is not NULL; + t.secondary1 is not NULL + { + "" + if prev_user_id is None + else + "AND t.user_id > :prev_user_id" + } + { + "" + if max_users is None + else + "LIMIT :max_users" + } + ; """ ) users = session.execute( - users_res, {"cnode_endpoint_string": cnode_endpoint_string} + users_res, + { + "cnode_endpoint_string": cnode_endpoint_string, + "prev_user_id": prev_user_id, + "max_users": max_users, + }, ).fetchall() users_dict = [dict(row) for row in users] return users_dict