Perforce as a Datastore, with Client-Side MVC
A colleague, Logan McGrath, has a blog series that scotch-tapes Perforce, Sinatra and AngularJS into a functional app. Another Colleague, Addison Lee, wrote a quick client polling example for it in Java. Although there is more work we could do on this thing, it is time to park it for now. As a proof of concept, we’re very happy. Someone else can productionize it.
Ive blogged before on what the application is used for – it’s for the configuration of a server stack. Here, I’ll reuse the picture from that entry:
Perforce is not designed for end-user applications. It is designed for developers, and to store source code (and related bits and pieces). We’re ignoring that, and using where we could have used CouchDB, Postgres or one of a hundred other choices. Why Perforce and not a SQL or a NoSQL database? Branches and merge-ability. Why Perforce and not Git/GitHub? Permissions per branch.
To use Perforce, we’ve had to honor it’s intentions around “working copy”. That is the set of files that are brought down from the server, than can be modified locally, before being sent back as a set. A user (who’s logged in) gets a separate set of files for themselves, and all of them are held on the server side, and will remain a work in progress until a commit or a revert is done. Pending changes will even survive the user logging off, and on again. Working copy, as Perforce would prefer, is actual files in a directory structure. As such they have to be permitted to only the daemon that’s running the app-config-app, and the user in question.
Perforce maintains lists of users (we’ve made no fancy enrollment pages). It also maintains the permissions on resources, directories and branches. Permissions can also be different for read versus write (individual resources, directories or branches). Again, we’ve done nothing to make this ‘administration’ piece a pretty ‘web’ experience. For permissions and enrollment cases, you’ll have to be adept with the perforce command line (p4) or the fat UI (P4V), or know someone who is.
Sinatra (Ruby) speaks HTTP, as Perforce does not. We use it to mount a number of RESTful endpoints, that serve and receive changed documents. The documents are originally received from Perforce (by Perforce command-line operations) and stored in that working copy directory (per-user). Users log in, and authenticated by Perforce. The Sinatra app also delegates to Perforce for the permissions as mentioned. In terms of lines of code, you couldn’t do it in much less.
Although we do use some templating on the server side, we could use less than we do and more AngularJS (see below). We also have implemented a read-cache.
This is very much my go-to web technology these days. At least, it is for apps that don’t require SEO (most internal applications don’t). It very cheaply GETs and POSTs a JSON document from/to the Sinatra web server.
Here’s all the technologies together in a diagram. One Process for the Perforce Daemon (p4d), one for Sinatra, and whatever browser are concurrently connected. The diagram shows two administrators – Fred and Wilma – and their separated working-copy.
Perforce is commercial.
Perforce has a per-user charge. It’s not amazingly costly, but you’ll have to factor it into your build-outs.
You either choose classic HTTP auth, or modern form-based auth. For humans, we’d ideally wish for the latter. For machines retrieving config-settings we’d want the former. Sinatra won’t allow both. If it did we’d detect the user-agent and force a workflow that’s appropriate.
Angular’s document management
Angular prefers POST over PUT. There is a workaround for that, but we’ve not used it. What we want to do is push up the document as it changes, so that Sinatra always has what is in the browser. We don’t want a “SAVE” button. For this app we don’t care that there could be a lot of POSTs – that’s not where the performance cost is. Unfortunately, Angular’s on-model-change equivalence will activate for individual key presses. That’s not what we want. We want it to be on-field-exit (as you tab out of a field), so we’ve hacked something that’s approximate. Logan gave some detail on this in one of the three blog entries to date.
The Angular page, is also the ‘spec’ for the document. There’s no schema on the backend, this is a trust-the-browser not to mess it up design. That’s not really a issue though, unless there’s a situation where Angular could pass back corrupt JSONto the the Sinatra layer.
Lack of a Sitemesh
Though Angular has mechanisms for dynamically tying content to parts of the URL to the right of the file-mapped resource, we’re not using them. We want finer control over the URLs. In fact we want the URL to very closely match the path to the resource in Perforce.
Subversion could be used instead of Perforce, it does HTTP via MOD_DAV_SVN (I’ve previously built a CMS with this and sitemesh). It has permissions, and command-line tool-abilty. TFS may could also be used, but I’m not sure about the command-line tool-ability. None of the DVCS tools can be used because of the lack of permissions per-branch. I love Git to pieces, but you could accidentally PULL from ‘production’ and PUSH to staging, thereby divulging production database passwords (or other secret things) to people that only have read-access to staging. That’s even true for GitHub ‘forks’.
Instead of Sinatra, there are about 1000 web frameworks in 30 languages that would do the job. The difference again is lines of code needed to get it working. We chose Sinatra, because it’s easy, and there’s no need for huge performance from this thing. On that last, you could argue that JVM-compiled solutions (or CLR, or direct C++ solution, or NodeJS etc etc) can all handle more concurrent requests, but you don’t need that here if you have the caching correct.