04 February 2007

It works

Well, CAATT is now "functional". After scratching my head over how to do the scheduler for a while I had a pretty good theory, so I sat down to code it yesterday afternoon. hence the post clarifying some of the design details. At the end of the night last night I had annual, semi-annual, quarterly and monthly recurring tasks scheduling themselves. It was about 500 lines of php code (I choose php because the rest of CAATT is written in php... it's a LAMP application really at this point, so all the database access methods were fresh in my head). This afternoon, between a trip to Urgent Care for my wife and the superbowl, I finished weekly, daily, and daily+weekends scheduling, and implemented skip scheduling for all. scheduler.sh (still a php script) is now only 127 lines. Why the drop? Because having written the scheduler for 4 virtually identical scheduling patterns I recognized the similarities and collapsed the core of the scheduler into a loop with a small switch statement that covers the only difference. The skip scheduling was virtually the same as the offset scheduling too, so it was factored into the same loop. That's what it really all came down to in terms of cutting it down to size... yesterday I wrote the first four scheduling protocols to learn how to do it. Today, I refactored them into common code, and then factored an entirely different set of scheduling protocols into it.


for ($lcv=1; $lcv < 8; $lcv++) {
$query[$lcv]['tasks']="select * from tasks where sched_process=$lcv";
$query[$lcv]['exist']="select work_id from work_queue where task=%s ";
}

$query[1]['exist'].="and year(input) = year(now())";
$query[2]['exist'].="and year(input) = year(now())
and (month(input) / 6 = month(now()) / 6)";
$query[3]['exist'].="and year(input) = year(now())
and (month(input) / 3 = month(now()) / 3)";
$query[4]['exist'].="and year(input) = year(now())
and month(input) = month(now())";
$query[5]['exist'].="and year(input) = year(now())
and month(input) = month(now())
and week(input,0) = week(now(),0)";
$query[6]['exist'].="and year(input) = year(now())
and month(input) = month(now())
and day(input) = day(now())";
$query[7]['exist'].="and year(input) = year(now())
and month(input) = month(now())
and day(input) = day(now())";

$today=date("N", time());
if ($today == 1 || $today == 7) {
$startat=7;
} else {
$startat=1;
}
//$startat=1; //for debug.

for ($lcv=$startat; $lcv < 8; $lcv++) {
echo "scheduling type $lcv \n";
$result = mysql_query($query[$lcv]['tasks']);
if ($results === false) die("FAILED: {$query[$lcv]['tasks']}\n". mysql_error());
while ($task = mysql_fetch_assoc($result)) {
echo " evaluating task {$task['task_id']}\n";

//look for it already assigned
$cmd=sprintf($query[$lcv]['exist'], $task["task_id"]);
$existing = mysql_query($cmd);
if ($existing === false) die("FAILED: $cmd\n". mysql_error());
$dosched=false;
$now['d'] = date("d", time());
$now['m'] = date("m", time());
$now['w'] = date("W", time());
$now['z'] = date("z", time());
if (mysql_num_rows($existing) == 0) {
if (isset($task['sched_offset'])) {
echo " task has offset\n";
$schedoff = strtotime($task['sched_offset']);
$schedat['d'] = date("d", $schedoff);
$schedat['m'] = date("m", $schedoff);
$schedat['w'] = date("W", $schedoff);

switch ($lcv) {
case 1: //annual
if ($schedat['d'] == $now['d'] &&
$schedat['m'] == $now['m'] ) $dosched = true;
break;
case 2: //half
if ($schedat['d'] == $now['d'] &&
$schedat['m'] % 6 == $now['m'] % 6 ) $dosched = true;
break;
case 3: //quarter
if ($schedat['d'] == $now['d'] &&
$schedat['m'] % 3 == $now['m'] % 3 ) $dosched = true;
break;
case 4: //month
if ($schedat['d'] == $now['d']) $dosched = true;
break;
case 5: //weekly
if ($schedat['w'] == $now['w']) $dosched = true;
break;
default: //daily, daily+weekend
die("task {$task['task_id']} doesn't make sense, it has an offset, but that isn't applicable!");
}
} elseif ($task['sched_skip'] != 0) {
echo " task has a skip\n";
mysql_free_result($existing);
$cmd="select work_id,date(input) as input from work_queue where task={$task["task_id"]} order by input desc limit 1";
$existing = mysql_query($cmd);
if ($existing === false) die("FAILED: $cmd\n". mysql_error());
$work = mysql_fetch_assoc($existing);
$lastassign = strtotime($work['input']);
$last['z'] = date("z", $lastassign);
$last['m'] = date("m", $lastassign);
$last['w'] = date("W", $lastassign);

switch ($lcv) {
case 4: //month
$lookat='m'; break;
case 5: //weekly
$lookat='w'; break;
case 6: //daily
case 7: //daily+weekend
$lookat='z'; break;
default: //annual, half, quarter
die("task {$task['task_id']} doesn't make sense, it has a skip, but that isn't applicable!");
}
if ($last[$lookat] + $task['sched_skip'] < $now[$lookat]) $dosched = true;

} else {
echo" task has no schedule offset or skip, and no satisfying schedule. yet....\n";
$dosched=true;
}
if ($dosched) {
echo " >> need to assign task ",$task["task_id"],"... ";
$cmd="insert into work_queue set task={$task["task_id"]}, doer={$task["usual_doer"]}";
mysql_query($cmd) or die ("FAILED: $cmd\n". mysql_error());
echo "inserted as work_id ",mysql_insert_id(),"\n";
} else {
echo " -- task does not need to be scheduled.\n";
}
} else { //there are satisfying instances in work_queue
echo " task {$task['task_id']} has ",mysql_num_rows($existing)," satisfying instances in work_queue: ";
while($work = mysql_fetch_assoc($existing)) echo $work['work_id']," ";
echo "\n";
}
mysql_free_result($existing);
}
mysql_free_result($result);
}


Now that I've finished writing the first draft of that, I read Wolf's take on programmers and why we write code. Wolf nails it on the head, as usual. It's not the writing code I enjoy, it's the problem solving, yesterday's code writing was a learning process, so that today I could really make it work. By the way, the above code (and all code for this project) was written with TextWrangler, and is covered under GPLv2.

Now all we need to do to make it useful is get some of the tasks around the house into it and start using it! I've already put many of mine... like the cat box, the water filters, the softener salt, the air filters, etc. And this isn't 100% complete... still missing the ability to lock out the users internet connection at the firewall until they're done with their chores, and the time/priority based scheduling (so you can put in a whole bunch of "once a week" jobs and tell the system to only allocate 100 minutes a day worth of work let's say and have *it* figure out what order to put those various weekly jobs in.) is still missing... but they're more "advanced features".

And of course, having just posted this, I now realize the first bug in the system... scheduled offsets in anything other than daily+weekends, will fail to be scheduled on the weekend. Oops.

Labels: ,

03 February 2007

random design points

I've been working on the hardest part of the caatt system... the scheduler. I had previously left the designations of some of the fields in the database's tasks table kinda up in the air. As I work on implementing it, I need them to solidify, so I'm documenting them here.

First off, I added an additional column, sched_skip of type tinyint. This is a convenience scheduling knob for things that are "every N intervals", like "every other day", "every 4 months", etc. (this removes the "bi-weekly" previously mentioned as a scheduling process.)

Sched_offset is another that was vague. It's meaning changes based on the value of sched_process.

When sched_process is 1 (annual) then the MONTH and DAY portion of sched_offset is the day of the year to schedule the task on. (really, it's the number of months and days into the period.)

When sched_process is 2 (half) then the MONTH and DAY portion of sched_offset is the offset of months and days into the period to schedule on. Ditto 3 (quarterly).

When sched_process is 4 (monthly) then the DAY portion of sched_offset is the offset into the month. Ditto 5 (weekly).

Labels: ,

29 January 2007

do what I asked you to do, not what you think I wanted

Sometimes software is just too smart for it's own good.

So as I'm working on coding the client for CAATT I was creating some test data in the database, and I found that my input timestamp keeps changing, even though I'm not touching it.


mysql> select * from work_queue;
+---------+---------------------+---------------------+------+----------+------+
| work_id | input | output | task | comments | doer |
+---------+---------------------+---------------------+------+----------+------+
| 1 | 2007-01-22 20:43:36 | 0000-00-00 00:00:00 | 1 | NULL | 501 |
| 2 | 2007-01-28 20:43:43 | 0000-00-00 00:00:00 | 2 | NULL | 502 |
| 3 | 2007-01-28 20:43:51 | 0000-00-00 00:00:00 | 4 | NULL | 501 |
| 4 | 2007-01-29 00:25:44 | 0000-00-00 00:00:00 | 5 | NULL | 501 |
+---------+---------------------+---------------------+------+----------+------+
4 rows in set (0.00 sec)

mysql> update work_queue set output=now(), comments="howdy" where work_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from work_queue;
+---------+---------------------+---------------------+------+----------+------+
| work_id | input | output | task | comments | doer |
+---------+---------------------+---------------------+------+----------+------+
| 1 | 2007-01-22 20:43:36 | 0000-00-00 00:00:00 | 1 | NULL | 501 |
| 2 | 2007-01-28 20:43:43 | 0000-00-00 00:00:00 | 2 | NULL | 502 |
| 3 | 2007-01-29 00:26:45 | 2007-01-29 00:26:45 | 4 | howdy | 501 |
| 4 | 2007-01-29 00:25:44 | 0000-00-00 00:00:00 | 5 | NULL | 501 |
+---------+---------------------+---------------------+------+----------+------+
4 rows in set (0.00 sec)


So I set the completion date and a comment on #3, but it changed the input time on me! ARGH!!! So I went back and looked at my table definition...


mysql> explain work_queue;
+----------+----------------------+------+-----+---------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+----------------------+------+-----+---------------------+----------------+
| work_id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| input | timestamp | NO | | CURRENT_TIMESTAMP | |
| output | timestamp | NO | | 0000-00-00 00:00:00 | |
| task | tinyint(3) unsigned | NO | | | |
| comments | varchar(4096) | YES | | NULL | |
| doer | smallint(5) unsigned | YES | | NULL | |
+----------+----------------------+------+-----+---------------------+----------------+
6 rows in set (0.00 sec)


Nope, nothing there to indicate why it did that. Off to the manuals I go, finally find this. GRRRR! show create table work_queue confirms that it set both the default AND the on update to CURRENT_TIMESTAMP when the table was created. Several attempts to modify the table fail to achieve anything even remotely close to what I want. In the end I did a mysqldump of the entire database and modified the creation script to be as such:


DROP TABLE IF EXISTS `work_queue`;
CREATE TABLE `work_queue` (
`work_id` bigint(20) unsigned NOT NULL auto_increment,
`input` timestamp NOT NULL default CURRENT_TIMESTAMP references work_queue on update NO ACTION,
`output` timestamp NOT NULL,
`task` tinyint(3) unsigned NOT NULL,
`comments` varchar(4096) default NULL,
`doer` smallint(5) unsigned default NULL,
UNIQUE KEY `work_id` (`work_id`)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;


So the magic bullet to create a mysql table that has a column that tracks INSERTION time, but NOT UPDATE TIME? A self referential on update no action. And you can only do that when you CREATE the table, there's no way to get alter to accept that as near as I can tell.

Now that that's out of my system, a quick update on the caatt clients: I've decided to do a quick LAMP implementation instead of a native OS/X client. It already knows who you are, asks if you've not told it, and remembers... and it can dump a list of work to do, with overdue item's highlighted separately. Details of a task and marking it complete were what I was working on when this stopped me. Oh yeah, and I slapped together a quick logo/favicon for it with OmniGraffle (and photoshop for cropping, since OmniGraffle's export sometimes picks bizare margins).

Labels: ,

05 January 2007

so how does it all come together

This is the next post in the thread on the chore allocation and tracking tool (CAATT, pronounced like "Kahn!!"). Previously I laid out the database schema for it, I've already put it into my database, and created two users, 'caatt' and 'caatts' which have rights to the database. Here's a dump of what's there:

mysql> explain tasks;
+---------------+----------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+----------------------+------+-----+---------+----------------+
| task_id | tinyint(3) unsigned | NO | PRI | NULL | auto_increment |
| short_name | varchar(128) | NO | | | |
| description | varchar(4096) | YES | | NULL | |
| minutes | tinyint(3) unsigned | NO | | | |
| sched_process | tinyint(3) unsigned | NO | | 0 | |
| sched_offset | date | YES | | NULL | |
| sched_order | tinyint(3) unsigned | YES | | NULL | |
| usual_doer | smallint(5) unsigned | YES | | NULL | |
+---------------+----------------------+------+-----+---------+----------------+
8 rows in set (0.00 sec)
mysql> explain work_queue;
+----------+----------------------+------+-----+---------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+----------------------+------+-----+---------------------+----------------+
| work_id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| input | timestamp | NO | | CURRENT_TIMESTAMP | |
| output | timestamp | NO | | 0000-00-00 00:00:00 | |
| task | tinyint(3) unsigned | NO | | | |
| comments | varchar(4096) | YES | | NULL | |
| doer | smallint(5) unsigned | YES | | NULL | |
+----------+----------------------+------+-----+---------------------+----------------+
6 rows in set (0.01 sec)
mysql> select User,Table_name,Table_priv from tables_priv where Db='caatt';
+--------+------------+----------------------+
| User | Table_name | Table_priv |
+--------+------------+----------------------+
| caatt | work_queue | Select,Update |
| caatts | work_queue | Select,Insert,Update |
| caatt | tasks | Select |
| caatts | tasks | Select |
+--------+------------+----------------------+
4 rows in set (0.00 sec)


I'll setup and maintain the tasks list by hand, as root, for now. The caatts user is the one that will assign work, the caatt user is the one that will be used for the general access clients.

But where did all those clients come from?

OK, so there will be two parts to the clients for this... a scheduler and a todo list. The scheduler will be a unix script, probably bash or perl, run by cron. It will come out and see what needs to be done, and add it to various lists, probably at like 4 in the morning or something. The big part of this will be the todo list gui. I'll write it as a cocoa gui. First though, I need to find an sql framework....

Labels: ,

03 January 2007

what's it all about?

This is the start of a sequence of posts I expect to create that talk about the process I'm following to create a fairly trivial little application for use in our house... the Chore Assigner and Tracker (CAT, until I come up with a better name for it). This is a little application that's been growing in the back of my head for a while now. Many people would start this by designing the GUIs. I tend to think that's kinda asinine. It leads to applications where the main selling point is that the app emits smoke effects while it burns a cd (poorly burns a cd from the sounds of things).

Instead I think the first thing you need to do is look at the application and ask "What the fuck does this thing *DO*?" Well... it assigns chores to people, and tracks that they're completed. Some day it might have hooks into the firewall so that certain machines can't surf the web until their users have finished their chores. Sounds like a database to me. So step 1... schema design.









TypeNullName
int PK task_id
str NO short_name
str NO description
int NO minutes
int NO sched_process
date OK offset
int OK sched_order
int OK usual_doer


There are a couple enums in that, sched_process and usual_doer. Assume usual_doer is a standard UID, those are standardized across the lan, so no problem there. the scheduling process however is slightly different:
  1. disabled
  2. annual
  3. half
  4. quarterly
  5. monthly
  6. weekly
  7. daily
  8. daily+weekends


So that defines what a chore (task) is, now we need a mechanism to track their assignment and completion... another table:







TypeNullName
int PK work_id
date NO in
date OK out
int NO task
int NO doer
str OK comment


I think this encapsulates all the information we need to store in the system. Of course I could be wrong, but that's what design/requirement reviews are for. I'll have one with the primary user on that schema shortly. (just did, neither of us see anything missing.)

I'll continue with the design of this application in a future post, I expect it to actually hit the code stage sometime soon.

Labels: ,