How to design task assignment system?

1.9k views Asked by At

I want to design a system which is similar with stackoverflow review feature. That is to say:

There are n tasks, which should assign to users (users count is unknown). At one time, one task should assign to at most one user, different users should not be assigned same task.

For example, n = 8, if one user enters the system default assign him 3 tasks.

  • At 17:00, Tom enters the system and get tasks 1, 2, 3.
  • At 17:01, Jim enters the system and get tasks 4, 5, 6.
  • At 17:02, Jerry enters the system and get tasks 7, 8.
  • At 17:03, Bob enters the system and get no task.
  • At 17:05, Tom completed task 1, 2, and leave the system.
  • At 17:06, Bob enters the system again and get task 3.

Suppose I use Database to store tasks info.

My solution is that when tasks 1, 2, 3 are assigned to Tom, delete the 3 records from DB and store them to memory. Then others will not get the 3 records. When Tom leaves the system, insert his completed tasks and uncompleted tasks to DB again (with task status "completed" or "uncompleted").

While the disadvantage is that store records to memory is not 100% safe, if the system crashed may cause data missing issue.

Could somebody know how stackoverflow designs review feature? Or share other solutions? I'm wondering whether SELECT ... FOR UPDATE is good in this use case.

1

There are 1 answers

1
APC On

What you need to implement is a FIFO stack or simple queue. In Oracle the best thing (unless you want to implement an actual queue with AQ) for such a thing is SELECT ... FOR UPDATE with the SKIP LOCKED clause. SKIP LOCKED allows us to easily operate a stack with multiple users.

Here's a simple interface:

create or replace package task_mgmt is

    function get_next_task return tasks.id%type;

    procedure complete_task (p_id in tasks.id%type);

    procedure release_task (p_id in tasks.id%type);

end task_mgmt;
/

This is a bare-bones implementation:

create or replace package body task_mgmt is

    function get_next_task return tasks.id%type
    is
        return_value tasks.id%type;
        cursor c_tsk is
            select id
            from tasks
            where status = 'open'
            order by date_created, id
            for update skip locked;

    begin
        open c_tsk;
        fetch c_tsk into return_value;
        update tasks
        set status = 'progress'
            , assigned = user
        where current of c_tsk;
        close c_tsk;
        return return_value;
    end get_next_task;

    procedure complete_task (p_id in tasks.id%type)
    is
    begin
        update tasks
        set status = 'complete'
            , date_completed = sysdate
        where id = p_id;
        commit;
    end complete_task;

    procedure release_task (p_id in tasks.id%type)
    is
    begin
        rollback;
    end ;

end task_mgmt;
/

Updating the status when the users pops the stack creates a lock. Because of the SKIP LOCKED clause, the next user won't see that task. This is a lot cleaner than deleting and re-inserting records.

Here's some data:

create table tasks (
    id number not null
    , descr varchar2(30) not null
    , date_created date default sysdate not null
    , status varchar2(10) default 'open' not null
    , assigned varchar2(30)
    , date_completed date
    , constraint task_pk primary key (id)
    )
/

insert into tasks (id, descr, date_created) values (1000, 'Do something', date '2015-05-28')
/
insert into tasks (id, descr, date_created) values (1010, 'Look busy', date '2015-05-28')
/
insert into tasks (id, descr, date_created) values (1020, 'Get coffee', date '2015-06-12')
/

Let's pop! Here's Session one:

SQL> var    tsk1 number;
SQL> exec :tsk1 := task_mgmt.get_next_task ;

PL/SQL procedure successfully completed.

SQL> print :tsk1

      TSK1
----------
      1000

SQL>

Meanwhile in Session two:

SQL> var    tsk2 number;
SQL> exec :tsk2 := task_mgmt.get_next_task ;

PL/SQL procedure successfully completed.

SQL> print :tsk2

      TSK2
----------
      1010

SQL>

Back in Session one:

SQL> exec task_mgmt.complete_task (:tsk1);

PL/SQL procedure successfully completed.

SQL> exec :tsk1 := task_mgmt.get_next_task ;

PL/SQL procedure successfully completed.

SQL> print :tsk1

       TSK
----------
      1020

SQL> 

The main drawback of this approach is that it requires users to maintain stateful sessions while they work on the task. It that's not the case then you need an API in which get_next_task() is a discrete transaction, and forget about locking.


Incidentally, it's probably better to let users grab a task rather than assign them through a logon trigger (or whatever you have in mind by "Tom enters the system and get tasks 1, 2, 3."). Pulling tasks is how the SO Review queue works.

Also, just assign one task at a time. That way you can get efficient distribution of work. You want to avoid the situation where Tom has three tasks on his plate, one of which he isn't going to complete, and Bob has nothing to do. That is, unless you're Bob.