What is new

in

Gaelyk 2.0

Vladimír Oraný

What's new in Gaelyk 2.0

  • Gaelyk in the nutshell 
  • (Surprise hot from the oven)
  • Breaking changes
  • Enhancements

Gaelyk in the nutshell

Lightweight toolkit for a Google App Engine

More information

Biggest application running Gaelyk

Serving 1.2M request per day



Glide

Breaking Changes


  • Groovy 2.0 support
  • Adding tasks asynchronously by default
  • Removed automatic @Version property
  • Single asterisk matching in routes
  • Minor API changes 

Groovy 2.0 Support


  • farewell GaelykCategory 
  • short cuts available wherever you want
  • @CompileStatic wherever possible

// use(GaelykCategory){ // no longer compile    MyPogo mypogo = new MyPogo(a: 'a', b: 'b', c: 'c')    Entity entity = mypogo as Entity    entity.save()// }



Script Extension Modules

Helper methods and tag-like functions
class ScriptExtension {    void ifAuthenticated(Script self, Closure body){        self.with{            if(user){                body(user)            }        }    }}
// someTemplate.gtpl<% ifAuthenticated { user -> %>
    Hello $user.nickname!<% } %>

Gradle FatJar Plugin

Deploying Gaelyk 2.0 needs Gradle FatJar 0.2+
 dependencies {
    classpath 'org.gradle.api.plugins:gradle-gaelyk-plugin:0.4.1'
    classpath 'org.gradle.api.plugins:gradle-gae-plugin:0.8', {
         exclude module: "gradle-fatjar-plugin"
    }
    classpath 'eu.appsatori:gradle-fatjar-plugin:0.2'
    classpath 'org.gradle.api.plugins:gradle-gae-geb-plugin:0.3'
}
Otherwise the extension modules 
descriptors are not merged

Adding Tasks Asynchronously


Before  2.0 the call was waiting for response...
 TaskHandle handle = defaultQueue << [url: '/task.groovy']

... even it was barely used
 defaultQueue << [url: '/task.groovy']

Since 2.0 the call returns future in the case anybody cares
 Future<TaskHandle> future = defaultQueue << [url: '/task.groovy']

Removed automatic @Version property

Version is automatic column for optimistic locking
It is pretty expensive to fetch (~1s)
@Entity class Book {    // magic long id property is still added    // @Key Long id    // no more version property added automatically    // @Version Long version    @Indexed String title}
Book book = new Book(title: 'It')assert book.hasProperty('id')assert !book.hasProperty('version')

More strict asterisk matching

No more greedy routes
get '/agent/*/@id',    forward: '/agent.groovy?id=@id'
'/agent/bond/7' => '/agent.groovy?id=7'
//before 2.0 the '*' in routes was greedy'/entry/james/bond/7' => '/agent.groovy?id=7'

//since 2.0 the '*' matches just to the closest slash
'/entry/james/bond/7' => '/agent.groovy?id=james/7'
// use '**' to get the old behaviour backget '/agent/**/@id', forward: '/agent.groovy?id=@id'

Minor API Changes

LifecycleManger shutdown hook
// use method call with closurelifecycle.shutdown{...}// instead of assigning closure as variable// lifecycle.shutdown = {...}

Parsing XMPP message XML payload
// use method call xml()message.xml()// instead of property access// message.xml

Enhancements


  • Search  
  • Datastore
  • Routes
  • Plugins
  • Parameter handling

Search

More options for indexes
Search DSL
Recoverable asynchronous search

Be sure you're running HRD!

Most of the examples only works when deployed

More options for indexes

search.index('restaurants').put {
    document(id: 'pakwaan') {
        title text: 'Pakwaan Indian Restaurant'        rating number: 4
        opensAt number: 10
        closesAt number: 24        averagePrice number: 7
        menu text: '''
Chicken Tikka 6 pcs - Marinated boneless chicken are with yoghurt, spices and cooked in tandoor.Chicken Seekh Kebab - Minced chicken with ginger flavour, lemon, and butter with green coriander        '''        // new
        tags atom: ['lunch menu', 'indian', 'take away']
        location geoPoint: [50.098009,14.403806]
    }}

Search on GAE

QueryOptions.Builder queryOptions = QueryOptions.newBuilder()
queryOptions.idsOnly = truequeryOptions.numberFoundAccuracy = 1000

SortOptions.Builder sortOptions = SortOptions.newBuilder()sortOptions.limit = 1000SortExpression.Builder exp = SortExpression.newBuilder() exp.expression = 'distance(location,geopoint(50.07543,14.436936))'exp.defaultValueNumeric = 0exp.direction = SortDirection.DESCENDING
sortOptions.addSortExpression(exp)
queryOptions.sortOptions = sortOptions
Query.Builder queryBuilder = Query.newBuilder()
queryBuilder.options = queryOptionsQuery query = queryBuilder.build('tags: "take away")

Search on GAE

SearchService search = SearchServiceFactory.searchService
Index index = search.getIndex( IndexSpec.newBuilder().setName(indexName).build() )
Results<ScoredDocument> results = index.search(query)def ids = results.results*.id

Search DSL

def results = search.search {    select ids
    from restaurants
    where tags =~ 'take away'
    sort asc by distance(location, geopoint(50.07543,14.436936)), 0    number found accuracy 1000    limit sort to 1000
}
def now = 22.5 // 22.30

def results = search.search {
    select title, opensAt, closesAt
    from restaurants
    where opensAt <= now
    and closesAt >= now
    sort desc by rating, 0
}

results.results*.title

Search DSL

def searchText = 'chicken'

def results = search.search {
    select snippet: snippet(query, menu),           distance: distance(location, geopoint(50.07543,14.436936))
    from restaurants
    where menu =~ ~searchText
    sort asc by distance(location,          geopoint(50.07543,14.436936)), Double.MAX_VALUE
}


for(result in results.results){
    println "$result.title: $result.snippet ($result.distance)"
}

Recoverable Async Search

def results = search.searchAsync(3) {
    select ids
    from restaurants
    where tags =~ 'take away'
    sort asc by distance(location, geopoint(50.07543,14.436936)), 0    number found accuracy 1000    limit sort to 1000
}
Future results = 3 * {    search.searchAsync {
       select ids
       from restaurants
       where tags =~ 'take away'
   }
}
Future key = 3 * { entity.asyncSave() }

Datastore

Query result lists and iterables
Restarting queries automatically
Saving QueryBuilder for later use
Parent support for @Entity
Better coercion performance
Cross-group transactions
Minor updates

Query result lists and iterables

Datastore operations now returns QueryResultList and QueryResultIterable where possible
QueryResultList<Entity> restaurants = datastore.execute {    from Restaurant    if(params.cursor){ startAt params.cursor }    limit Math.min((params.limit ?: 10) as int, 100)}request.restaurants = restaurants
<ul>    <% for(r in request.restaurants){ %>        <li>$r</li>     <% } %></ul><a href="/restaurants?cursor=${request.restaurants}">Next</>

Restarting queries automatically

Queries can run only for limited time
No problem in standard requests
Throws exception in e.g. mapper tasks
// works only for iterate methoddef result = datastore.iterate {    from Restaurant    restart automatically}for(Entity r in restaurants){    if(!r.processed){        r.processed = true    }    r.save()}

Saving QueryBuilder for later use

QueryBuilder builder = datastore.build {    from Restaurants}def results = nullint counter = 0int curusor = nullwhile(!results || counter < 10){    counter++    try {        if(cursor){            builder.startAt cursor        }        results = builder.execute()    } catch(e) {        log.warning "$e"    }}

Parent support for @Entity

@Entity class Meal {    @Parent Key restaurant    @Key String name    @Indexed Double price    @Indexed Double weight}
Meal meal = Meal.get(['Restaurant','pakwaan'] as Key, 'chicken masala')
List<Meal> meals = Meal.findAll { ancestor ['Restaurant','pakwaan'] as Key where weight > 200}Meal.delete(['Restaurant','pakwaan'] as Key, 'chicken masala')
@Parent works for POGO coercion too

Better coercion performance

New DatastoreEntity interface to skip relfection
Implemented by @Entity classes automatically
Implement manually if you want performance boost
@Entity class Person {
    String firstName
    String lastName
    Date born
    Double height
}1000.times{ (person as Entity) as Person }
coercion without DatastoreEntity interface ~ 1 s
coercion with DatastoreEntity interface ~ 250 ms
(with @Version property cca 10 x slower)


Cross-group transactions

Only works on HRD
datastore.withTransaction(true){   Entity payment = new Entity('Payment')   payment.amount = price   Key paymentKey = payment.save()
Entity order = new Entity('Order') order.payment = paymentKey.id order.save()}

Minor updates

Ignoring static properties
class POGO implements Serializable {    // @Ignore no longer needed    static final long serialVersionUID = 1L}
Gracefull unindexed accessor
entity.name = 'Vladimir'      // set indexed propertyprintln entity.unindexed.name // no longer throws exception

Routes

Optional routes variables
Routes indexes

Optional routes variables

get '/posts/@year?/@month?/@day?/page-@page?', forward: '/posts.groovy'
Expands to

get '/posts/@year/@month/@day/page-@page',
  forward: '/posts.groovy?year=@year&month=@month&day=@day&page=@page'

get '/posts/@year/@month/page-@page', forward: '/posts.groovy?year=@year&month=@month&page=@page'
get '/posts/@year/page-@page', forward: '/posts.groovy?year=@year&page=@page'
get '/posts/page-@page', forward: '/posts.groovy?page=@page'
// to be continued

Optional routes variables

get '/posts/@year/@month/@day',
  forward: '/posts.groovy?year=@year&month=@month&day=@day'
get '/posts/@year/@month',
  forward: '/posts.groovy?year=@year&month=@month'    
get '/posts/@year',
  forward: '/posts.groovy?year=@year'
get '/posts',
  forward: '/posts.groovy'
 

Routes indexes

get '/home',        forward: 'home.groovy'   // index: 0get '/about',       forward: 'about.groovy'  // index: 1

...
// new notation for extension matching// these routes will be evaluated firstall '/**/*.gtpl', ignore: true, index: -101all '/**/*.groovy', ignore: true, index: -100

Plugins

Handling groovlet's return value
Prioritize plugin's routes

Handling groovlet's return value

after {
if(result instanceof Map){ result.each{ k,v -> request[k] = v } }}
after {
    if(request.renderAsJson) {
        response.contentType = 'application/json'
        if(result instanceof Map && result?.status) {
            response.status = result.remove('status')  
        }
        JsonBuilder json = new JsonBuilder()
        json result
        json.writeTo(response.writer)
    }
}

Handling groovlet's return value

if(!users.userLoggedIn){    return [error: 'You must be logged in first', status: 401]}
if(!users.userAdmin){ return [error: 'You must be admin!', status: 401]}
[result: datastore.execute{ from Stuff where owner == user}]

Prioritize plugin's routes

Some aplications may override plugin's routes
get "/@username", forward: "/profile.groovy?username=@username"
So you can move the plugin's routes before the apps one
startRoutingAt -30000

get "/_ah/gaelyk-console/",   forward: "/org/gaelyk/console/script.gtpl"     // index: -30000
  get "/_ah/gaelyk-console/render/",  forward: "/org/gaelyk/console/template.gtpl"   // index: -29999

Parameter handling

Safe as Type conversion
// ?tag=one&tag=two
assert params.tag.collect { it } == ['one', 'two']

// ?tag=one assert params.tag.collect { it } == ['o', 'n', 'e'] // ?limit=100 assert params.limit as int == 100 // ?limit=100&limit=200 try { params.limit as int assert false } catch(ClassCastException e) { // cannot cast String[] to int assert true }

Parameter handling

Safe as Type conversion
// ?tag=one&tag=two
assert (params.tag as String[]).collect { it } == ['one', 'two']

// ?tag=one assert (params.tag as String[]).collect { it } == ['one'] // ?limit=100 assert params.limit as int == 100 // ?limit=100&limit=200 assert params.limit as int == 100

Thank you

Vladimír Oraný


What is new in Gaelyk 2.0

By musketyr

What is new in Gaelyk 2.0

  • 5,380