Drupal

Catching Apache segfaults due to eAccelerator

Last night we migrated our Linode for Gamers With Jobs to a new Xen VPS and we've noticed a significant performance boost. We did, however, start encountering a random issue with segmentation faults in Apache. If you haven't seen this happen before, it tends to begin innocently with one Apache process dying, and therefore giving errors (usually WSOD), but quickly balloons into dozens of dead processes. It essentially hoses Apache.

Apparently the issue is due to eAccelerator, so I reinstalled it and cleared its caches in the hope that it might limit its occurrence. Just in case, though, 2bits has a great fix for it, using the logwatcher script by Firebright, Inc. I was able to quickly get it going, and the only difference is that I used the Debian init.d script provided by Derek Laventure to run it.

Taxonomy Search 2.0 module release

For a project at work, we needed to be able to manage, and therefore search, large-scale taxonomies (10,000+ terms). Users needed to be able to search for term names, descriptions and synonyms, so I figured a module using the Drupal search API seemed to be the best bet for a solution. I dove in deep and came back up with Taxonomy Search 2.x.

I've just released version 2.0 of the module, and you can check it out at the Taxonomy Search project page. I'd love some feedback, as this is my first module that utilizes the search API, and there may be some rough edges. Please take a look at let me know what you think!

Slides for ASU presentations

Attached below are the slides I used for both the March and April ASU Drupal Users' Group presentations I did. The first was on Content Access and Workflows and discussed the setting up effective content creation workflows with corresponding access controls. The second covered an intro Content Creation Kit (CCK) and Views. See the lists below for modules / sites referenced in the presentations.

Content Access and Workflows presentation

Simple Workflow + Actions

Workflow NG

Other stuff referenced

CCK & Views presentation

Referencing an array in a variable object property

Update:
Thanks to an anonymous commenter for letting me know that this is an issue of operator precedence.

This is more a personal note than anything, but I've been banging my head against a wall trying to figure out how to reference an array within a variably-named object property in PHP. Having not found anything very useful when search Google, I figure my post may end up being someone's helpful search result. Maybe I just don't know the right terminology for what I'm trying to do...?

Anyways, I've got a module that needs to modify the string in a CCK text field before it's shown to users on the node edit form. It's a "glue" module that helps us handle course enrollment, and the field in question handles course instructor(s) via a comma-delimited list of usernames. The module takes the user's submitted data, parses it into an array and stores each username in a table joined with course ID for other uses (such as passing to our Sakai installation). The CCK field is referenced in several places, so I use an admin settings form to allow us to say, "This CCK field is the field that users fill out to define instructors." This allows us to avoid hard-coding the CCK field name all over our glue module, but it also led to the headache I encountered today.

When a user goes back to the form to edit the course, I want to present the username list cleanly (alphabetical, no accidental whitespace, etc). Here's the code I tried to use:

function ideal_courses_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  // ... (irrelevant stuff here)

    case 'prepare':
      $field_instructors = variable_get('ideal_courses_field_instructors', NULL);
      // The line that fails is below:
      $node->$field_instructors[0]['value'] = ideal_courses_instructors_as_string($node);
    break;

  // ... (more irrelevant stuff here)
}

The error I kept getting was, "PHP Fatal error: Cannot use string offset as an array." The strange thing is that doing a print_r($node->$field_instructors); works fine. The fix was simple, but difficult to find: wrap the variable property name in curly braces: $node->{$field_instructors}[0]['value']. The full result is below:

function ideal_courses_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  // ... (irrelevant stuff here)

    case 'prepare':
      $field_instructors = variable_get('ideal_courses_field_instructors', NULL);
      // Fixed line is below:
      $node->{$field_instructors}[0]['value'] = ideal_courses_instructors_as_string($node);
    break;

  // ... (more irrelevant stuff here)
}

Initial releases of Signup Scheduler and Status modules

I've just put the finishing touches on the 1.0 releases of the Signup Scheduler and Signup Status modules, which I've written about before. While they're both in just their initial release, I feel pretty good about their stability and utility. We've been using both modules in production on the IDEAL site, where users can register for professional development courses. I've also been getting lots of great feedback and bug reports from community members in the issue queues. Special thanks to Mark Dowsett for testing and issue reporting and Derek Wright for working with me on some helpful changes to the Signup module.

The project pages have plenty of details, but most notably the Signup Status module contains lots of goodies that weren't mentioned in my original post, such as plenty of Views integration, signup certificates, signup status change emailing, and more. Check it out if you're interested!

Just got rolling with a VPS on Linode (Part 2)

Now that I had a demonstrably working and functional web server going on my Linode (see Just got rolling with a VPS on Linode (Part 1)), it was time to get the rest of my toolkit on the box, setup users and secure the server a bit.

Installing Subversion and migrating repositories

Well, installing Subversion couldn't be any simpler:

apt-get install subversion

Login to old server and dump current repositories:

svnadmin dump /path/to/repository > repository.dump

Copy dump file to new server, and on new server:

svnadmin create /path/to/repository
svnadmin load /path/to/repository < /path/to/dump/repository.dump

Adding users and groups

I decided I didn't want to be logged in as root all the time, especially since I'll most likely be bringing some other folks in to work on the server in the future. So, I setup the admin group, created myself a new user and put myself in both the admin and staff groups.

addgroup admin
adduser jrbeeman
usermod -G staff,admin jrbeeman

Next, I wanted to make sure admins could sudo to root, so that they could install programs and do other root-y things. The sudoers file, as far as I can tell, can only be edited with the command visudo:

visudo

...and added the line:

%admin  ALL=(ALL) ALL

Setting up the firewall

This was probably the least-traveled territory in the whole VPS setup for me. Thankfully, there is an awesome resource in the website IP Tables Rocks, with a full rundown of how to lock down unneeded ports. It emphasizes locking down everything, and then only opening up those services you want open. I essentially followed the tutorial, but proceeded to lock down every port except those that I knew I would need for web services and working with the server (22, 80, 443, etc.)

Performance

By this point, I've started working on getting the Gamers With Jobs development site migrated over, and I'm working on nailing down any performance issues. As I said in part 1, the main reason for going to a VPS was the sheer size and load on the GWJ site and how shared hosting was really hosing the speed. Most of the tweaks from here on out are related to the GWJ site.

Tweak MySQL settings

Since the Gamers With Jobs site is very database intensive, getting MySQL to perform optimally given the site's load is important. I'm still tweaking these settings here and there, but here's what I'm at so far. I'm attempting to go for large enough buffers and caches to keep things snappy, but without bloating out the caches to the point that things slow down.

#
# * Fine Tuning
#
key_buffer              = 256M
max_allowed_packet      = 16M
thread_stack            = 128K
thread_cache_size       = 8
#max_connections        = 100
table_cache             = 256
thread_concurrency      = 4
sort_buffer_size        = 1M
read_buffer_size        = 1M
read_rnd_buffer_size    = 4M
myisam_sort_buffer_size = 64M
#
# * Query Cache Configuration
#
query_cache_limit       = 1M
query_cache_size        = 16M
#
# Turn on slow query logging to help track down performance killers
#
log_slow_queries        = /var/log/mysql/mysql-slow.log
long_query_time = 5
#
# Some further table-type tweaks
#
[isamchk]
key_buffer = 128M
sort_buffer_size = 128M
read_buffer = 2M
write_buffer = 2M

[myisamchk]
key_buffer = 128M
sort_buffer_size = 128M
read_buffer = 2M
write_buffer = 2M

Bringing over the GWJ site required quite a bit of scripting of INSERT and DELETE statements that fudged with table lengths, so I also optimized all the tables with free data space:

-- Get the table names...
SHOW TABLE STATUS WHERE Data_free > 0;
-- ...and run the following for each
OPTIMIZE TABLE table_name;

Tweak Apache settings

The YSlow utility from Yahoo is a great way to track down potential end-user performance issues, so I ran it against the GWJ dev site and tweaked quite a few things to improve the rating and speed reported there.

First, I needed to enable a few Apache modules:

a2enmod deflate
a2enmod expires
a2enmod cache

Then, in /etc/apache2/httpd.conf, I added the following lines to the stanza of the GWJ virtual host definition:

# Gzip html, css, js, etc.
AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml application/x-javascript application/json
# Set expires headers on html, css, js, etc.
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType text/html "access plus 1 seconds"
  ExpiresByType image/gif "access plus 1 month"
  ExpiresByType image/jpeg "access plus 1 month"
  ExpiresByType image/png "access plus 1 month"
  ExpiresByType text/css "access plus 1 week"
  ExpiresByType text/javascript "access plus 1 month"
  ExpiresByType application/x-javascript "access plus 1 month"
</IfModule>
# Set ETags
FileETag MTime Size

Install memcached

In order to squeeze a bit more performance out of the server, I decided to install memcached and the related Drupal module, which allows you to configure Drupal to store certain cache data in memory. I essentially followed the instructions in Robert Douglass's article on Lullabot, but with a couple of modifications.

First, libevent1-1.3b and memcached-1.2.1-1 can be installed via apt-get on Ubuntu gutsy, all with:

apt-get install memcached

Then, I enabled the Apache module:

a2enmod mem_cache

Install eaccelerator

Not much to write here, aside from noting that I followed the great article on 2Bits to get going.

Done... sorta

Seeing how I started writing this article a couple of weeks ago and am just getting around to publishing it, I think I'll call it "finished," for now. I hope that someone out there finds this useful!

Useful TextMate Snippets for Drupal

I took a few minutes this evening to whip up some TextMate snippets that I think will be useful for Drupal development. They're nothing fancy, and certainly not as ambitious as Steven Witten's kitchen sink approach, but I think it'll simplify one of the most repetitive tasks I usually find myself in when writing a new module.

These snippets all use TextMate's built-in tab stop handling, which allows you to quickly insert the snippet and then just tab through all the relevant places where you'd need to change something. Give it a try - especially with the .module file snippet.

Initial setup

  1. Open up TextMate
  2. Go to the Bundle Editor (Bundles -> Bundle Editor -> Show Bundle Editor, or ctrl+option+cmd+b)
  3. Add a new bundle and call it "Drupal"
  4. From here out, each snippet can be added by choosing "New Snippet"

The .info file

I called this one "Module info file" and gave it the tab trigger drupmod_info. I haven't figured out how to get this one to actually render via a tab trigger, but it's easy enough to just navigate to the Drupal bundle and choose the snippet.

; \$Id\$
name = "${1:Example module}"
description = "${2:Example module description}"
dependencies = $0

The .module file

This snippet uses TextMate's variable mirroring to let you quickly rename all the hooks in the snippet with the first tab stop. I used the hooks below because most of my modules implement these hooks. Just follow the pattern for naming, etc. to add your preferred hooks.

I called this one "Module general hooks" and assigned it the tab trigger drupmod_m.

<?php
// \$Id\$

/**
 * Implementation of hook_perm
 */

function ${1:example}_perm() {
  return array(
    '${2:example permission}',
  );
}


/**
 * Implementation of hook_menu
 */

function ${1:example}_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/${1:example}',
      'title' => t('${3:Example}'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array('${1:example}_admin_form'),
      'access' => user_access('administer site configuration'),
    );
  }

  return $items;
}


/**
 * Menu callback - Admin settings form.
 */

function ${1:example}_admin_form() {
  $form = array();
  return system_settings_form($form);
}


/**
 * Implementation of hook_form_alter
 */

function ${1:example}_form_alter($form_id, &$form) {
  switch ($form_id) {
    // Alter node edit form
    case $form['type']['#value'] .'_node_form':
    break;
  }
}


/**
 * Implementation of hook_nodeapi
 */

function ${1:example}_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'delete':
    break;

    case 'insert':
    break;

    case 'load':
    break;

    case 'submit':
    break;

    case 'update':
    break;

    case 'view':
    break;
  }
}

The .install file

This one is pretty similar to the .module file, but I tried to make sure it handled some common uninstall tasks I usually forget to tackle until I absolutely have to.

I called this one "Module install hooks" and assigned it the tab trigger drupmod_i.

<?php
// \$Id\$

/**
 * Implementation of hook_install
 */

function ${1:example}_install() {
  switch ($GLOBALS['db_type']) {
    case 'mysqli':
    case 'mysql':
      db_query("CREATE TABLE IF NOT EXISTS {${1:example}} (
        id int unsigned NOT NULL auto_increment,
        field varchar(128) NOT NULL default ''
        PRIMARY KEY (id),
      ) /*!40100 DEFAULT CHARACTER SET utf8 */;"
);
    break;
  }
}


/**
 * Implementation of hook_uninstall
 */

function ${1:example}_uninstall() {
  db_query("DROP TABLE {${1:example}}");
  $variables = db_query("SELECT name FROM {variable} WHERE name LIKE '${1:example}%%'");
  while ($variable = db_fetch_object($variables)) {
    variable_del($variable->name);
  }
}

Just got rolling with a VPS on Linode (Part 1)

Note: A large part of this is taken from Victor Kane's article on Awebfactory about setting up Drupal on a fresh Linode, but I've documented some other things here and did some things a little differently than he did, so I figured it'd be worth writing up a post on the process. I've kept the details thin here in places where Victor's notes are more than satisfactory, but I've made sure to note where that happens.

Update: Be sure to check out part 2 of this article, as well.

I've spent the last several months of my off-work hours plugging away at helping the folks over at Gamers With Jobs get rolling with an upgraded version of Drupal, and in the process we decided to move from a shared hosting environment to a place where we've got a lot more control over performance and site configuration. In the meantime, Victor Kane's article on getting Drupal up and running on a Linode came across my RSS reader and provided the kick in the pants I needed to really investigate it. I looked at several VPS options, but in the end Linode seemed to be the best. They offered a seven day money back guarantee, which honestly isn't much, but it was long enough for me to feel comfortable giving it a shot without being out sixty bucks, so I decided to try it out.read more...

Signup Scheduler and Status modules

Over the past couple of weeks I've put a ton of time into a couple of modules that extend the functionality provided by the fantastic Signup module. Both of these modules come as a result of requirements defined by a project I've got going at work. We're creating a course catalog and the Signup module was our pick for how users "register" for courses. The base module is pretty solid, but we needed some additional functionality to meet the requirements of the registration system.

Signup Scheduler provides users with the ability to administer a node's signups (either by being the node creator or by being a global signup administrator) to set open and close dates for signups on the node. Signups are then opened and closed based on the defined schedule via cron.

While the scheduler module is pretty slick, I'm really excited about the Signup Status module - mostly because it solves several problems for our registration system. The goal of the module is to add the concept of "status" to a user's signup to a node, with status meaning any number of things. User's could be marked as "complete," allowing the node creator (in this case, course instructor) to mark a user as having completed a course. Other states could be "incomplete," "no-show," etc. By default, the module provides two states, "Approved" (the default state) and "Wait Listed," and any number of other states can be added by site administrators that users can then assign to signed-up users.

This functionality essentially provides two major requirements for us - arbitrary status assignment and wait listing. I've still got a bit more work to do on the wait listing functionality to make it really work without confusing the node creator, but essentially the idea is that the "wait listed" state, and any other states an administrator creates, could be marked as modifying the signup total. This will allow node creators to specify, for example, that a course has 10 wait list seats available.

The Signup Scheduler module provides some other notable functionality, as well. Node creators can now print a roster of attendees and signup other users for the node. It also provides what I hope will be a solid API for other modules to tie into, including a couple of hooks that allow other modules to act upon the status change for a user. I'm hoping to spend some time helping getting the base Signup module itself to a state where it has some nice integration points in it like this, too. For example, right now it's not possible to tie into the event of a user's signup being cancelled, or the event of signing-up a user (at least as far as I can tell).

There will be some more fun stuff along these lines over the coming days and weeks, so stay tuned!

Organic Groups - Select Audience by Group Type

Last week I posted up a new Drupal module that attempts to improve the usability of the Organic Groups module when it's used with multiple group types and large numbers of groups. The module modifies the node edit form's audience selector, provided by the Organic Groups module, such that the groups are selectable by content type. This is useful on sites with a very large number of groups and many different group types. For example, a site like the one this was written for, with Districts, Schools and Interest Groups (all of which are content types defined as organic groups) will have have a node edit form with an individual selector for each group type.

Check it out, if you're an Organic Groups user, and let me know what you think.

Syndicate content