[programming] Implementing a reddit like comment system

  • Thread starter Thread starter Deleted member 923340
  • Start date Start date
D

Deleted member 923340

Guest
I've been trying to implement "a reddit style comment system" as a fun side project.
And surprisingly it is one the hardest "design logic" I've come across.

ZVUnLx0


To start off it seems simple enough. You have the number of votes for each comment, and the parent-id.

The fuckary happens when you try to think about the fetching logic. You don't want to query 1000s of comments in a single request. Pagination is quite easy when the data is a simple list. But it gets way trickier when you have to work to work with nested shit.

If you look at reddit - they truncate the list with "X more replies" while listing "most voted" comments on top. This thing happen in a recursive manner.

I cannot wrap my head around how they have designed this logic.

Programmers of BHW, come help me figure this shit out.
 
I am assuming you are using some kind of data store where you can query.

1. Store the level of nesting along with the comment. Use that while querying.

2. Make decisions in your code based on the order of magnitude of the total number of comments. 100 comments can be fetched and loaded quickly, regardless of levels, as opposed to 10,000 comments.

3. Based on the number, use appropriate data structures and algorithms (so point #2 is about dynamic decision branches, while this is about the actual decisions).

The rest needs to be fine-tuned, based on your architecture, infrastructure, environment, programming language, and other factors. There won't be a one-size-fits-all solution. I am sure you already realize that though. :)
 
I am assuming you are using some kind of data store where you can query.
Sure, it can be anything from postgresql, cassandra, or dynamodb.

Based on the number, use appropriate data structures and algorithms
This doesn't answer the question :) You gotta be specific.

How would you sort based on upvotes + achieve pagination (load X more comments) efficiently when number of comments are in the range of thousands.. (like reddit does).

C-like:
struct Comment {
    id: String,
    content: String,
    parent_id: Option<String>, // it means this can be optional in rust-lang
    upvotes: u32,
    nesting: u16, // how would this help?
    poster_id: String
}

I'm looking for ideas in regards to how do u store comments in a way that is easier to paginate. (load X more comments)
 
It can still be stored as a list. You create a row such as:

comment_id, post_id, ident_lvl, parent_comment_id.

When you insert comment into database, you just check for parent comment's ident_lvl and decrease it.

To fetch it, you first count number of comments per ident and then fetch number of comment accordingly.

This is one oversimplified explanation, but should push you into general direction.
 
To fetch it, you first count number of comments per ident and then fetch number of comment accordingly.
Wouldn't doing this for each comment be expensive?

Imagine there are 1000s of comments with some of them are nested with into <10 levels.
 
This doesn't answer the question :) You gotta be specific.

The specifics will depend on your programming language and environment. :)

Let me explain.

Reddit's comment implementation is no mystery - it is available here:

https://github.com/reddit-archive/reddit/blob/master/r2/r2/models/comment_tree.py
[EDIT: Queries here: https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/queries.py - you can browse the whole code here: https://github.com/reddit-archive/reddit]

Code:
class CommentTreePermacache(object):
    @classmethod
    def _permacache_key(cls, link):
        return 'comments_' + str(link._id)

    @classmethod
    def _mutation_context(cls, link):
        """Return a lock for use during read-modify-write operations"""
        key = 'comment_lock_' + str(link._id)
        return g.make_lock("comment_tree", key)

    @classmethod
    def prepare_new_storage(cls, link):
        """Write an empty tree to permacache"""
        with cls._mutation_context(link) as lock:
            # read-modify-write, so get the lock
            existing_tree = cls._load_tree(link)
            if not existing_tree:
                # don't overwrite an existing non-empty tree
                tree = {}
                cls._write_tree(link, tree, lock)

    @classmethod
    def _load_tree(cls, link):
        key = cls._permacache_key(link)
        tree = g.permacache.get(key)
        return tree or {}   # assume empty tree on miss

    @classmethod
    def _write_tree(cls, link, tree, lock):
        assert lock.have_lock
        key = cls._permacache_key(link)
        g.permacache.set(key, tree)

    @classmethod
    def get_tree_pieces(cls, link, timer):
        tree = cls._load_tree(link)
        timer.intermediate('load')

        cids, depth, parents = get_tree_details(tree)
        num_children = calc_num_children(tree)
        num_children = defaultdict(int, num_children)
        timer.intermediate('calculate')

        return cids, tree, depth, parents, num_children

    @classmethod
    def add_comments(cls, link, comments):
        with cls._mutation_context(link) as lock:
            # adding comments requires read-modify-write, so get the lock
            tree = cls._load_tree(link)
            cids, _, _ = get_tree_details(tree)

            # skip any comments that are already in the stored tree and convert
            # to a set to remove any duplicate comments
            comments = {
                comment for comment in comments
                if comment._id not in cids
            }

            if not comments:
                return

            # warn on any comments whose parents are missing from the tree
            # because they will never be displayed unless their parent is
            # added. this can happen in normal operation if there are multiple
            # queue consumers and a child is processed before its parent.
            parent_ids = set(cids) | {comment._id for comment in comments}
            possible_orphan_comments = {
                comment for comment in comments
                if (comment.parent_id and comment.parent_id not in parent_ids)
            }
            if possible_orphan_comments:
                g.log.error("comment_tree_inconsistent: %s %s", link,
                    possible_orphan_comments)
                g.stats.simple_event('comment_tree_inconsistent')

            for comment in comments:
                tree.setdefault(comment.parent_id, []).append(comment._id)

            cls._write_tree(link, tree, lock)

    @classmethod
    def rebuild(cls, link, comments):
        """Generate a tree from comments and overwrite any existing tree."""
        with cls._mutation_context(link) as lock:
            # not reading, but we should block other read-modify-write
            # operations to avoid being clobbered by their write
            tree = {}
            for comment in comments:
                tree.setdefault(comment.parent_id, []).append(comment._id)

            cls._write_tree(link, tree, lock)


class CommentTree:
    def __init__(self, link, cids, tree, depth, parents, num_children):
        self.link = link
        self.cids = cids
        self.tree = tree
        self.depth = depth
        self.parents = parents
        self.num_children = num_children

    @classmethod
    def by_link(cls, link, timer=None):
        if timer is None:
            timer = SimpleSillyStub()

        pieces = CommentTreePermacache.get_tree_pieces(link, timer)
        cids, tree, depth, parents, num_children = pieces
        comment_tree = cls(link, cids, tree, depth, parents, num_children)
        return comment_tree

    @classmethod
    def on_new_link(cls, link):
        CommentTreePermacache.prepare_new_storage(link)

    @classmethod
    def add_comments(cls, link, comments):
        CommentTreePermacache.add_comments(link, comments)

    @classmethod
    def rebuild(cls, link):
        # retrieve all the comments for the link
        q = Comment._query(
            Comment.c.link_id == link._id,
            Comment.c._deleted == (True, False),
            Comment.c._spam == (True, False),
            optimize_rules=True,
        )
        comments = list(q)

        # remove any comments with missing parents
        comment_ids = {comment._id for comment in comments}
        comments = [
            comment for comment in comments
            if not comment.parent_id or comment.parent_id in comment_ids
        ]

        CommentTreePermacache.rebuild(link, comments)

        link.num_comments = sum(1 for c in comments if not c._deleted)
        link._commit()

Look at the different versions of CommentTreeStorage - look at the comments.

Version 2 was abandoned "because counters ended up being unreliable and the shards put too much GC pressure on
the Cassandra JVM."

Version 3 was abandoned "because of more unexpected GC problems after longer
time periods and generally insufficient regular-case performance."

Plus, I am pretty sure their current version of these classes will not look like this anymore.

The point is, you will not see those problems, until you have a real-time live environment.

You are looking at this as a side project - so at this point, anything will work.
Use linked lists. Use maps and dictionaries. Darn, even tries will work with some code gymnastics.

It is when you move to production that you will need to tweak things.

That is why I gave some (admittedly) abstract, general guidelines, based on my own experience in changing code to accommodate growing live sites.

Once you have a real production environment, it makes sense to talk about specifics, based on the specific problems you are experiencing.
Because, your live site will not have the exact same problems as mine, even if we both use the same language and environment. :)
 
Last edited:
The specifics will depend on your programming language and environment. :)

Let me explain.

Reddit's comment implementation is no mystery - it is available here:

https://github.com/reddit-archive/reddit/blob/master/r2/r2/models/comment_tree.py
[EDIT: Queries here: https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/queries.py - you can browse the whole code here: https://github.com/reddit-archive/reddit]

Code:
class CommentTreePermacache(object):
    @classmethod
    def _permacache_key(cls, link):
        return 'comments_' + str(link._id)

    @classmethod
    def _mutation_context(cls, link):
        """Return a lock for use during read-modify-write operations"""
        key = 'comment_lock_' + str(link._id)
        return g.make_lock("comment_tree", key)

    @classmethod
    def prepare_new_storage(cls, link):
        """Write an empty tree to permacache"""
        with cls._mutation_context(link) as lock:
            # read-modify-write, so get the lock
            existing_tree = cls._load_tree(link)
            if not existing_tree:
                # don't overwrite an existing non-empty tree
                tree = {}
                cls._write_tree(link, tree, lock)

    @classmethod
    def _load_tree(cls, link):
        key = cls._permacache_key(link)
        tree = g.permacache.get(key)
        return tree or {}   # assume empty tree on miss

    @classmethod
    def _write_tree(cls, link, tree, lock):
        assert lock.have_lock
        key = cls._permacache_key(link)
        g.permacache.set(key, tree)

    @classmethod
    def get_tree_pieces(cls, link, timer):
        tree = cls._load_tree(link)
        timer.intermediate('load')

        cids, depth, parents = get_tree_details(tree)
        num_children = calc_num_children(tree)
        num_children = defaultdict(int, num_children)
        timer.intermediate('calculate')

        return cids, tree, depth, parents, num_children

    @classmethod
    def add_comments(cls, link, comments):
        with cls._mutation_context(link) as lock:
            # adding comments requires read-modify-write, so get the lock
            tree = cls._load_tree(link)
            cids, _, _ = get_tree_details(tree)

            # skip any comments that are already in the stored tree and convert
            # to a set to remove any duplicate comments
            comments = {
                comment for comment in comments
                if comment._id not in cids
            }

            if not comments:
                return

            # warn on any comments whose parents are missing from the tree
            # because they will never be displayed unless their parent is
            # added. this can happen in normal operation if there are multiple
            # queue consumers and a child is processed before its parent.
            parent_ids = set(cids) | {comment._id for comment in comments}
            possible_orphan_comments = {
                comment for comment in comments
                if (comment.parent_id and comment.parent_id not in parent_ids)
            }
            if possible_orphan_comments:
                g.log.error("comment_tree_inconsistent: %s %s", link,
                    possible_orphan_comments)
                g.stats.simple_event('comment_tree_inconsistent')

            for comment in comments:
                tree.setdefault(comment.parent_id, []).append(comment._id)

            cls._write_tree(link, tree, lock)

    @classmethod
    def rebuild(cls, link, comments):
        """Generate a tree from comments and overwrite any existing tree."""
        with cls._mutation_context(link) as lock:
            # not reading, but we should block other read-modify-write
            # operations to avoid being clobbered by their write
            tree = {}
            for comment in comments:
                tree.setdefault(comment.parent_id, []).append(comment._id)

            cls._write_tree(link, tree, lock)


class CommentTree:
    def __init__(self, link, cids, tree, depth, parents, num_children):
        self.link = link
        self.cids = cids
        self.tree = tree
        self.depth = depth
        self.parents = parents
        self.num_children = num_children

    @classmethod
    def by_link(cls, link, timer=None):
        if timer is None:
            timer = SimpleSillyStub()

        pieces = CommentTreePermacache.get_tree_pieces(link, timer)
        cids, tree, depth, parents, num_children = pieces
        comment_tree = cls(link, cids, tree, depth, parents, num_children)
        return comment_tree

    @classmethod
    def on_new_link(cls, link):
        CommentTreePermacache.prepare_new_storage(link)

    @classmethod
    def add_comments(cls, link, comments):
        CommentTreePermacache.add_comments(link, comments)

    @classmethod
    def rebuild(cls, link):
        # retrieve all the comments for the link
        q = Comment._query(
            Comment.c.link_id == link._id,
            Comment.c._deleted == (True, False),
            Comment.c._spam == (True, False),
            optimize_rules=True,
        )
        comments = list(q)

        # remove any comments with missing parents
        comment_ids = {comment._id for comment in comments}
        comments = [
            comment for comment in comments
            if not comment.parent_id or comment.parent_id in comment_ids
        ]

        CommentTreePermacache.rebuild(link, comments)

        link.num_comments = sum(1 for c in comments if not c._deleted)
        link._commit()

Look at the different versions of CommentTreeStorage - look at the comments.

Version 2 was abandoned "because counters ended up being unreliable and the shards put too much GC pressure on
the Cassandra JVM."

Version 3 was abandoned "because of more unexpected GC problems after longer
time periods and generally insufficient regular-case performance."

Plus, I am pretty sure their current version of these classes will not look like this anymore.

The point is, you will not see those problems, until you have a real-time live environment.

You are looking at this as a side project - so at this point, anything will work.
Use linked lists. Use maps and dictionaries. Darn, even tries will work with some code gymnastics.

It is when you move to production that you will need to tweak things.

That is why I gave some (admittedly) abstract, general guidelines, based on my own experience in changing code to accommodate growing live sites.

Once you have a real production environment, it makes sense to talk about specifics, based on the specific problems you are experiencing.
Because, your live site will not have the exact same problems as mine, even if we both use the same language and environment. :)
See, if I didn't push you, you wouldn't have come up with this :D
Thanks mate.

I knew about the opensource repo. I just didn't want to look at it. Coz it is 10 year+ old + plus i wanted to see if i can figure it on my own.

I pick interesting problems and try to implement for fun, so I don't get dumb ( an partially i enjoy the process).

I was thinking of using a Key-Value database like dynamodb/cassandra for this. And after thinking a lot I think i figured out a way to do this efficiently.

---------

This is my solution for anyone that comes across this post later. This may look weird to people with SQL background tho.

Look how I have mapped the sortkey for replies. Dynamodb has a "Begins_with" function that can query result in mili-second latency even for gigabytes of data.
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html


RedditComments.png
 
Back
Top