Tuesday, 10 September 2013

Building an assessment app in 2+ɛ days -- dealing with Heroku's slow Scala build times

(This post is retrospective -- students started using it yesterday.)

While Impressory is just running on an AWS instance, I put Assessory for our class on Heroku so that it would get an https address immediately *, and there wouldn't be a delay while I waited for DNS settings for a new domain name to propagate.

Heroku is a Platform-as-a-Service provider that uses a push to deploy mechanism. Add Heroku as a remote to the git repository, and then…

git push heroku master

…and Heroku will build and deploy your code.

This works for a number of platforms, including Scala Play apps such as this one.

In theory.

In practice, Heroku can be very slow building a Scala Play app, even one as simple as this, and it would regularly take longer than the 15 minute maximum that Heroku allows. In which case, Heroku would reject the push, and even though the update might only have been seconds away from going live, I'd be frustratingly bunted back to starting the deployment again.

The modular nature of the app, while great for keeping the code tidy, also seemed to slow down Heroku's builds as it has to go through an update cycle (resolving dependencies) for each of the modules as it compiles them. These seem to take a bit of time on Heroku.

Avoiding Heroku's long build times

The short answer to avoiding Heroku taking an age to compile an app (and often having to compile the compiler interface before it starts), it turns out, is this: don't let it compile it at all.

There's two ways of "not letting it".

  1. Apparently there's an alternative build mechanism for Heroku called Anvil.

    This uses Heroku's build packs on some other AWS servers. It seems to get around the timeout but still takes some time.

    To be honest, I didn't get time to look at it until later, as I needed to get an update up quickly. When I did try it out, while writing this post, it did work -- though it took its merry time too.

  2. The other is a hacky little workaround that involves almost no set-up, and is very much faster.

    Don't push the Play app itself to Heroku. Instead, publish it as a JAR file to a Maven or Ivy repository. Play apps can be published as JAR files just by running this from sbt:

    + publish
    

    This tends to be very fast because you've already compiled your code locally before you decide you want to upload it. (And your development machine is probably quicker than the AWS machines that Heroku builds on.)

    Then we create a second, essentially empty Play app that has our real app as its only dependency. We're treating our app as a library that's used by a trivial wrapper app. (We include only a very few files that we need to be a valid Play app: application.conf, plugins.sbt, build.properties. Perhaps one or two others, but they are straight copies of the files in our "real" app.)

    We push our wrapper app to Heroku, and Heroku will happily fetch our application code -- already compiled and packaged -- from the repository when it does its dependency resolution, as it would any other library. HTTP calls to the almost-empty outer app be served by the code in our JAR file -- including requests for the minified Javascript. Bingo, our app is up and running. Deployments take less than a minute because there is nothing for Heroku to compile.

    If you're not keen on pushing your code to a public Maven or Ivy repository, then you can push it to a local repository that you include in the almost-empty Heroku app.

This second approach feels like cheating, but in practice the only downside I've noticed so far is that public assets (images) from our "real" app would now be served out of the JAR file -- which is slower than serving them straight from a file if the app wasn't packaged up as library.

But in Assessory there are only three images that get served anyway -- the cartoon drawings on the NotFound, Forbidden, and InternalServerError screens. (And as it happens I messed up the URL so they're not appearing in the dependent-JAR-file version because of a double // in the path. The image below is from the un-JARed version in a previous post.)

This is one of the only pages in the app with an image

So I think for a while, I'm just going to put my few static images up somewhere else on the web -- perhaps in Cloudinary, and keep using this dependent app trick to make Assessory deployments fast.

* If a user accesses a site using http rather than https on a passwordless WiFi network (such as UQ's visitor WiFi), then it's possible to intercept their session key over the air. This is fairly well-known, and it's why Facebook, Google and others have moved to https only. For Assessory, where students are marking each other, I'd like to ensure that an https URL is available.

Monday, 9 September 2013

Building an assessment app in 2+ɛ days -- students started using it yesterday. (Going retrospective)

Students started using the app to critique each others' projects yesterday, as planned. Though I hadn't done a demo in the previous lecture as I'd hoped. So it wasn't two days, but it got there in time to be useful.

The screenshot below is from the form for editing the questionnaire -- as I was struggling to find screenshots that wouldn't reveal student data I should keep hidden.

For instance, if I clicked on "Allocations" I'd get a neet little list of which students are allocated to review which groups, whether they've logged in and linked their GitHub accounts, and which reviews they've started writing. But I don't want students knowing who is reviewing them, so I can't publish a picture of that to the web!

I guess another one I can show you is this:

Those pictures down the bottom are the GitHub avatars of users in those groups who have logged in (and the pre-enrol system has spotted them and automatically added them to their groups). The pictures are funny blocky images because these ones have been generated by GitHub for users who haven't set their avatar picture.

Most of the groups appear to be empty. This just means I took the picture less than a day after advertising the app to students. The pre-enrol system means that students are automatically added into their groups when they visit the course page. When I took the picture, 23 students had already logged in, but I cropped the image just before the first student who had uploaded an avatar (to avoid publishing people's photos or drawings on my blog.)

Going retrospective

Anyway, the next few posts will be retrospective -- looking back on the app that's been built rather than blogging as I go.

Friday, 6 September 2013

Building an assessment app in 2+ɛ days -- refining the concepts

The nineteenth commit is up, but it's high time I started discussing the design of the app itself.

(I think I've been through all the technical odds and ends. There's also support for server-sent events and websockets, but I'm not putting that in this app.)

Screenshot at latest commit

Trying to keep it simple, the app centres around Groups and Tasks in a course. So that's what students will see on the course's Assessory page.

It turns out there are some interesting relationships between tasks and groups.

The first task

The first task I'm going to need to write is the group peer critique task.

Students are in two kinds of group:

  • A Tutorial Group

    There are two tutorial sessions, with approximately half the
    class in each

  • A Project Group

    Each project group has 3 to 5 students in it

(These categories correspond to "Group Sets" in Assessory.)

Groups are going to be presenting their work in the tutorial on Monday. That means that the critique task has to care about both group sets -- it has to allocate each student to review another project from the same tutorial.

If it allocated them the same project group, well you can't assess yourself; and if it allocated them a group from the other tutorial, they wouldn't be there to see the presentation.

The second task

When students critique each other's work, they also get to critique the critique.

The second task we want is for each student to read the critiques their group has received, and mark whether or not they were constructive and useful.

So back to it...

So, now all the course and group pre-enrolments are in and working, it's time to get these tasks written.

Thursday, 5 September 2013

Building an assessment app in 2+ɛ days -- 16th commit

Right, back to it then… After the fun of the CEO's visit this morning, (and a big long sleep last night) back to work on the assessment app.

The students are going to be using it on Monday, so that's a deadline I can't let whoosh past me, as it's not just me making up a deadline for myself.

User updates in a functional world

The commit I just pushed changes course pre-enrolments so they happen automatically when the browser asks for the user's courses.

That means this is a request that changes something about the user -- it changes the user's registration.

I've written the app in a way that is functional and typically works with immutable data. That means there's one small additional wrinkle in this request.

DataAction.returning.one automatically converts an item to JSON for the requesting user. It uses an Approval[User] that typically has a LazyId for the user, fetched the first time it's needed.

But, I happen to have written this particular app in a functional style -- with immutable data objects. (You don't have to write your app with immutable data types, I just did for this one.) And this request modifies the user.

If your data types are immutable, you can find yourself with a small bug where this happens:

  1. We ask for the user, because to look up any pre-enrolments, we need a list of their social identities

  2. This triggers the lazy reference to the user to load.

  3. We find a pre-enrolment in the database, and update the user's registrations.

  4. We return the course from DataAction.returning.one

  5. But the Approval in the request has an immutable representation of the user that was fetched before we registered them to the new course, and the JSON comes out as if they weren't registered.

The solution involves a change to one line of code, and the addition of two more:

  1. Change the method from DataAction.returning.one to DataAction.returning.json

    (or from DataAction.returning.many to DataAction.returning.manyJson)

  2. Create a new approval for the updated user.

  3. Call the JsonConverter with the new Approval

We can see this in CourseController.myCourses

def myCourses = DataAction.returning.manyJson 
{ implicit request =>

  val userAfterUpdates = for (
    u <- request.user; 
    updated <- doPreenrolments(u)
  ) yield updated

  // As we've updated the user, we'll need a new Approval
  val approval = Approval(userAfterUpdates)

And at the end of the method:

  approved <- approval ask Permissions.ViewCourse(c.itself);
  j <- CourseToJson.toJsonFor(c, approval)
) yield j

Wednesday, 4 September 2013

Building an assessment app in 2+ɛ days -- 13th commit

This is a little picture of where the app is up to at the moment. (This is the admin screen for a course.) After the lecture this morning, and a little more coding, I'm going to pop home and take a kip, and resume updates a little later.

The last few commits haven't been especially interesting to blog about -- just adding more of the DAO classes, services, and controllers, in much the same style as the previous ones.

However, I have been making a few design decisions for how the app will behave that might make interesting reading later on today.

Is that a whooshing noise?

A couple more commits have gone in, though I haven't blogged them.

Course pre-enrolments happen, but I haven't yet set up group pre-enrolments or the critique task itself. Those will need to happen later today instead.

So, it looks like it'll need to be an assessment app in three days.

Building an assessment app in two days -- 8th commit

A few yawns are creeping in here, it's getting late…

The eighth commit is up, and now we can create courses. The interesting part of this commit, however is security.

Security in Assessory

If you have a look in CourseController, you'll see controllers that look like this:

/**
 * Retrieves a course
 */  
def get(id:String) = dataAction.one { 
  implicit request =>     
    val cache = request.approval.cache
    for (
      course <- cache(refCourse(id));
      approved <- request.approval ask 
                  Permissions.ViewCourse(course.itself)
    ) yield course
}

The permissions check is chained right there in the for loop (which is syntactic sugar for chaining flatMap calls on the Refs)

The way of thinking about it is that at any stage you can ask for approval to do something. That approval might be given; it might take some time to work out (involve looking something up in the database) and it might fail or be refused. All those fit neatly into the functionality of Ref, so we treat is asking for a Ref[Approved].

This also means it's independent of the database or Play classes, and I've declared the permission rules in the assessory-api module.

Security is in the API

If you look in Permissions, you can see the different permissions that an Approval[User] can ask to be approved.

Sometimes these are straightforward objects:

case object CreateCourse extends Perm[User] {    
  def resolve(prior:Approval[User]) = {
    Approved("Anyone may create a course")
  }
}

And sometimes they are approvals on an item:

  case class ViewCourse(course:Ref[Course]) 
       extends PermOnIdRef[User, Course](course) 
  {
    def resolve(prior:Approval[User]) = 
      hasRole(
         course, prior.who, 
         CourseRole.student, prior.cache
      )
  }

Approvals on an item (PermOnIdRef) are clever enough to realise that if you ask for an approval on Course(id=1).itself, and you ask for an approval on LazyId(classOf[Course], "1"), those are the same approval and it doesn't need to look up the ID the second time.

And it can do that independently of what kind of database you've wired up, or whether or not it's in a Play app.

Cache

As well as remembering granted approvals, Approval also contains a cache for Ref lookups.

The Approval tends to be present in all three of the controller, the security check, and the JSON conversion -- and these are the three places where you typically need to look up Refs. So having a cache attached to it is rather handy.

JSON conversion is seemless with security too

All this flatMapping on Refs has another payoff in the JSON conversion.

If you have a look at CourseToJSON, you'll see that it embeds a permissions block into the JSON it returns

def toJsonFor(c:Course, a:Approval[User]) = {

  val permissions = for (
    view <- optionally(
      a ask Permissions.EditCourse(c.itself)
    );
    edit <- optionally(
      a ask Permissions.EditCourse(c.itself)
    )
  ) yield Json.obj(
    "view" -> view.isDefined,
    "edit" -> edit.isDefined
  )

  for (p <- permissions) yield {
    courseFormat.writes(c) ++ 
    Json.obj("permissions" -> p)
  }
}

This happens asynchronously -- the user reference in the approval is asynchronous and may or may not already have been retrieved.

The JSON block that this produces ends up looking like something this:

{
  "id":"52275e6a9acb3a4500d7c2ea",
  "title":"Design Computing Studio 2",
  "shortName":"DECO2800",
  "shortDescription":"…",
  "addedBy":"522732099acb3a4d00fedff9",
  "created":1378311786259,
  "permissions": {
    "view":true,
    "edit":true
  }
}

Because the permissions block is in the item, on the client it is easy to enable and disable components using Angular.js.

Say, for instance, we might have an edit link that only shows if the edit permission is present:

<a href="edit" ng-show="course.permissions.edit">
  Edit
</a>