Step 14 deploy to gae

opensas edited this page Nov 3, 2011 · 9 revisions

Step 14 - deploy to gae

Purpose: in this step we will see how to deploy you app to google's application engine cloud computing platform.

cd ~/devel/apps
git clone git://github.com/opensas/play-demo.git
git checkout origin/14-deploy_to_gae

Create a gae application

Go to https://appengine.google.com/ (you wil obviously need a google account) and click in create application.

You might be asked to enter a mobile number, in my case (Argentina) I had to enter 541122223333, being 54 the argentina prefix, 11 the Buenos Aires prefix, and 2222333 the mobile number withour the leading 15.

Once you set up your app, download gae sdk 1.4 from here.

Take into account that gae needs a 1.6 java version.

Following the installation explained at setting up your development environment, unzip it to ~/devel/opt and create a symbolic link to it, like this (of course you can install it anywhere you want)

ln -s ~/devel/opt/appengine-java-sdk-1.4.0/ ~/devel/gae

Gae doesn't get along too well with JPA, so it's recommended to user another data layer. In our case, as it's recommended in play's documentation, we'll use siena.

Edit your dependencies file like this:

## Application dependencies

require:
    - play -> crud
    - play -> siena [2.0.0,)
    - play -> crudsiena [2.0.0,)
    - play -> gae 1.4
    - provided -> DateHelper 1.0 
repositories: 
    - provided: 
        type:       local 
        artifact:   "${application.path}/jar/[module]-[revision].jar" 
        contains: 
            - provided -> * 

And from the command line run:

play deps --forceCopy --sync

This will tell play to install all the dependencies.

The --sync options deletes every file not recognized as a dependency, in our case it will remove the crud module and replace it with crudsiena. The --forceCopy option tells play to install every module in the application's directory, instead of installing it in the play framework installation folder.

Migrating models to Siena

Now we have to update our project to replace every reference to JPA with Siena.

So in our models we remove every import, the @Entity and @ManyToOne annotations, and we also have to create the findById method, like this:

[...]
public class Event extends EnhancedModel {

	@Id(Generator.AUTO_INCREMENT)
	public Long id;
[...]
	public Event findById(Long Id) {
		return all().filter("id", id).get();
	}
[...]

Notice how we extend from EnhancedModel from siena package. Be sure to always import siena packages instead of javax.persistence.

The siena.EnhancedModel is just like the siena.Model, but it automatically adds the followgin helper method that is needed by siena:

        public static Query<Event> all() {
                return Model.all(Event.class);
        }

Migrating queries to Siena

Siena syntax for querying the datasource is a little bit different, so we'll have to do a couple of adjustments. Do the following replacements (we left commented out the original syntax)

controllers.Application

    public static void list() {
    	Secure.loadUser();
    	//List<Event> events = Event.find("order by date desc").fetch();
        List<Event> events = Event.all().order("-date").fetch();
        render(events);
    }

[...]
    public static void nextEvent() {
    	//Event nextEvent = Event.find("date > ? order by date", new Date()).first();
    	Event nextEvent = Event.all().filter("date >", new Date()).order("date").get();
    	render(nextEvent);
    }
[...]
	@SuppressWarnings("unused")
	@Before
	private static void loadEventTypes() {
		//renderArgs.put("types", EventType.find("order by name").fetch());
		renderArgs.put("types", EventType.all().order("name").fetch());
	}

also in controller.Secure replace

            OAuth2.Response response = FACEBOOK.retrieveAccessToken(authUrl);
            //User user = User.find("accessToken = ?", response.accessToken).first();
            User user = User.all().filter("accessToken", response.accessToken).get();
            if ( user == null ) {
                user = new User();

As you can see, all these are trivial changes.

Siena does not automatically populate related objects, so you have to do it by yourself. Modify views/Application/list.html

            <td>${event.id}</td>
            <td>#{a @form(event.id)}${event.name}#{/a}</td>
            <td>${models.EventType.findById(event.type.id)?.name}</td>
            <td>${event.type}</td>
            <td>${event.place}</td>

You also have to adjust the Bootstrap job

lib.utils.BootstrapJob

	@Override
	public void doJob() {
		//Fixtures.deleteAllModels();
		//Fixtures.loadModels("data.yml");
		SienaFixtures.deleteAllModels();
		SienaFixtures.loadModels("data.yml");
	}

Don't forget to comment the db configuration in application.conf, in order for siena to use gae's datastore instead of the in memory h2 database.

conf/application.conf

#   - fs  : for a simple file written database (H2 file stored)
# db=mem

when starting the app you should see the following message

01:42:17,860 INFO  ~ The backing store, /home/sas/devel/apps/play-demo/tmp/datastore, does not exist. It will be created.

Then map the /admin route to the crudsiena module, instead of the crud module

conf/routes

*       /admin                                  module:crudsiena

You can also modify /crudsiena-2.0.2/views.CRUD/layout.html tempalte to update jquery to the latest version.

And that's it, we converted our whole app from JPA to Siena.

Set application name and version

When you run the gae application for the first time, a war folder will be created. Inside you'll find a WEB_INF folder with a appengine-web.xml file. Set the application name and version.

<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
	<application>jugar-demo</applicatn>
	<version>1</version>
</appengine-web-app>

Gae allows you to have many version running at the same time. Go to https://appengine.google.com/, choose your app, click on Versions, and there you can delete version as well as set the default one. To access a specific version you can prefix the url with the version, like this: http://2.jugar-demo.appspot.com

Deploying to GAE

To deploy your app, just run the following command:

play gae:deploy --gae=/home/sas/devel/gae

You can optionally set GAE_PATH environment variable to omit --gae parameter.

Go ahead and test your app at gae. This application is available at http://jugar-demo.appspot.com/.

Setting a cron job

Gae module currently doesn't support play's jobs. But there's an easy workaround. Just create a cron.xml file to tell gae to run a specific task at regular intervals. For more info see here.

Save a file cron.xml in WEB-INF directory of your application (alongside appengine-web.xml) with the following contents:

<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
  <cron>
    <url>/load</url>
    <description>Repopulate demo data every 30 minutes</description>
    <schedule>every 3 minutes</schedule>
  </cron>
</cronentries>

caching next event to speed things up (and avoid google pricing!)

Our app is issuing an http request every 2 seconds to display the next event, and accordingly the action Application.nextEvent access the datastore to retrieve the info.

It's wise to cache this database access, not only from a performance point of view, but also because of google policy pricing.

So we'll create two private methods to get the next event and save it to the cache

[...]
    private static Event getNextEvent() {
        Event nextEvent = (Event) Cache.get("nextEvent");
        if (nextEvent==null) {
            nextEvent = refreshNextEvent();
        }
        return nextEvent;
    }
    
    private static Event refreshNextEvent() {
        final Event nextEvent = Event.all().filter("date>", new Date()).order("date").get();
        Cache.set("nextEvent", nextEvent);
        return nextEvent;
    }

then we'll have to update the methods that modify events to refresh cached next event

[...]
    public static void delete(Long id) {
        final Event event = Event.findById(id);
        event.delete();
        refreshNextEvent();
        list();
    }
[...]
    public static void save(@Valid Event event) {
        if (validation.hasErrors()) {
            render("@form", event);
        }
        event.save();
        refreshNextEvent();
        flash.success("event successfully saved!");
        list();
    }

    public static void nextEvent() {
        final Event nextEvent = getNextEvent();
        render(nextEvent);
    }
    
    public static void loadFromYamlFile() {
        new BootstrapJob().doJob();
        refreshNextEvent();
        list();
    }
[...]

that's it, our app is finnaly running on gae


Now we are going to Step 15 - next steps.