Perfect-ZooKeeper

This project implements an express Swift library of ZooKeeper

This package builds with Swift Package Manager and is part of the Perfect project.

Release Note

This project can only be built on ⚠️ Ubuntu 16.04 ⚠️ . Mac OS X doesn't support it.

Quick Start

Swift Package Manager

Add Perfect-ZooKeeper to your project's Package.swift:

.Package(url: "https://github.com/PerfectlySoft/Perfect-ZooKeeper.git", majorVersion: 1)

Import Library

Import Perfect-ZooKeeper library to your source code:

import PerfectZooKeeper

Debug

To debug your ZooKeeper application, Perfect-ZooKeeper provides a static method called debug to set the debug level, for example:

// this will set debug level to the whole application
ZooKeeper.debug()

You can also adjust the debug level to method debug(_ level: LogLevel = .DEBUG) by a parameter:

  • level: LogLevel, the debug level, could be .ERROR, .WARN, .INFO, or .DEBUG by default

Log

To trace and log your ZooKeeper application, Perfect-ZooKeeper provides a static method called log, for example:

// this will redirect the debug information to standard error stream
ZooKeeper.log()

The only parameter of log(_ to: UnsafeMutablePointer<FILE> = stderr) is to, a FILE pointer as in C stream, it is stderr by default but you can redirect it to any available FILE streams.

Using ZooKeeper Object

Before performing any actual connections, it is necessary to construct a ZooKeeper object:

let z = ZooKeeper()

Or alternatively, you can also add a timeout setting to such an object, which defines the maximal milliseconds to wait for connection:

// indicates that the connection attempt will be treated as broken in eight seconds.
let z = ZooKeeper(8192)

Connect to ZooKeeper Hosts

Use ZooKeeper.connect() to connect to specified hosts. Take example, the demo below shows how to connect to a ZooKeeper host, and how the program invokes your callback once connected:

try z.connect("servername:2181") { connect in
  switch(connect) {
  case .CONNECTED:
    // connection is made
  case .EXPIRED:
    // connection is expired
  default:
    // connection is broken
  }
}

⚠️ NOTE ⚠️ , you may also connect to a cluster of host by replacing the above connection string into a string of multiple hosts in such an expression: "server1:2181,server2:2181,server3:2181", but this may be subject to the ZooKeeper version that you are connecting with. For more information of ZooKeeper connection string, see ZooKeeper Programmer's Guide

Existence of a ZNode

Once connected, you may check a specific ZNode by calling exists(), for example:

let a = try z.exists("/path/to")
print(a)

This function will return a Stat() structure if nothing wrong, for example: swift // this is a sample result of calling print(try z.exists("/path/to")) Stat(czxid: 0, mzxid: 0, ctime: 0, mtime: 0, version: 0, cversion: -1, aversion: 0, ephemeralOwner: 0, dataLength: 0, numChildren: 1, pzxid: 0)

List Children of a ZNode

Method children() may list all available direct sub nodes under the objective and put them into an array of string.

let kids = try z.children("/path/to")
// if success, it will list all sub nodes under /path/to in an array.
// for example, if there is /path/to/a and /path/to/b,
// then the result is probably ["a", "b"]
print(kids)

Save Data to a ZNode

As a key-value directory, each ZNode may contain a small amount of data in form of a string, usually not exceed to 10k. You can save your own configuration data into a ZNode, synchronously or asynchronously, as demanded.

Save Data Synchronously

Synchronous version of save() will return a Stat() structure if success:

let stat = try z.save("/path/to/key", data: "my configuration value of key")
print(stat)

Parameters of func save(_ path: String, data: String, version: Int = -1) throws -> Stat: - path: String, the absolute full path of the node to access - data: String, the data to save - version: Int, version of data, default is -1 which indicates ignoring the version info

Save Data Asynchronously

Asynchronous version of save() has all the same parameters with an extra StatusCallback but without returning value:

try z.save("/path/to/key", data: "my configuration value of key") { err, stat in
  guard err == .ZOK else {
    // something wrong
  }
  guard let st = stat else {
    // async save() returns a null status
  }
  // print the status after saving
  print(st)
}

Load Data from a ZNode

Similar to save(), the ZooKeeper load() also has both synchronous version and asynchronous version as well:

Load Data Synchronously

To load data from a ZNode synchronously, simply call load("/path/to"), and it will return a tuple of (value: String, stat: Stat), which stands for data value and status of the node:

let (value, stat) = try z.load("/path/to")

Load Data Asynchronously

The data loading from a ZNode asynchronously will require an extra callback with a parameter of (error: Exception, value: String, Stat) as demo below:

try z.load(path) { err, value, stat in
  guard err == .ZOK else {
    // something wrong
  }//end guard
  guard let st = stat else {
    // there is no status information of node
  }//end guard
  print(st)
  // this is the actual data value as a String
  print(value)
}//end load

Make a Node

Function func make(_ path: String, value: String = "", type: NodeType = .PERSISTENT, acl: ACLTemplate = .OPEN) throws -> String can build different type of nodes, with writing data value and set ACL (Access Control List) info for this node in the same moment. Here are the parameters:

  • path: String, the absolute full path of the node to make
  • value: String, the value to store into node
  • type: NodeType, i.e., .PERSISTENT, .EPHEMERAL, .SEQUENTIAL, or .LEADERSHIP, which means ephemeral + sequential. Default type is .PERSISTENT
  • acl: ACLTemplate, basic ACL template to apply in this incoming node, i.e., .OPEN, .READ or .CREATOR. Default is .OPEN, which means nothing to restrict

⚠️ Note ⚠️ The return value will be the newly created pat, i.e., the same one as input if type is .PERSISTENT or .EPHEMERAL, and will be appended with a serial number only if the node type is .SEQUENTIAL or .LEADERSHIP.

Make a Persistent Node

The following code demonstrates how to create a persistent node with data:

let _ = try z.make("/path/to/key", value: "my config data value for this key")

Make a Temporary Node

A temporary ZNode means it will automatically disappear once the session was over (usually after a few seconds of disconnection). To create such a node, simply add a node type parameter to call:

let _ = try z.make("/path/to/tempKey", value: "data for this temporary key", type: .EPHEMERAL)

Make a Sequential Node

A Sequential ZNode means if you want to create a /path/to/key node, it will return a /path/to/key0123456789, i.e., a 10 digit number will be added to the node you named.

let path = try z.make("/path/to/myApplication", type: .SEQUENTIAL)
print(path)
// if success, the path will be something like `/path/to/myApplication0000000123`

⚠️ Note ⚠️ Sequential node is persistent and can be removed only by calling remove() method explicitly.

Make a Leadership Node

The purpose of leadership node is to select a leadership server among all candidates. Similar to .SEQUENTIAL, the leadership node is also a temporary node, which help all clustered backups to determine who shall be the leader among the cluster by checking whose serial number is the minimal one, which means who is the first available.

let path = try z.make("/path/to/myApplication", type: .LEADERSHIP)
print(path)
// if success, the path will be something like `/path/to/myApplication0000000123`
// and will be deleted automatically once disconnected.

Remove a Node

Method func remove(_ path: String, version: Int32 = -1) enables the function to delete an existing node:

// this action will result in a removal regardless node versions.
try z.remove("/path/to/uselessNode")

Watch for Changes

Perfect-ZooKeeper provides a useful function watch() to monitor other instance operations against a specific node. The full API of watch() method is func watch(_ path: String, eventType: EventType = .BOTH, renew: Bool = true, onChange: @escaping WatchCallback) with parameter explained below:

  • path: String, the absolute full path of the node to watch
  • eventType: watch for .DATA or .CHILDREN, or .BOTH
  • renew: watch the event for once or for ever, false for once and true for ever.
  • onChange: WatchCallback, callback once something changed

For example:

try z.watch("/path/to/myCheese") { event in
  switch(event) {
  case CONNECTED:
    // me myself just connected to this node???
  case DISCONNECTED:
    // connection is broken
  case EXPIRED:
    // connection is expired
  case CREATED:
    // this shall never happen - just created for the node me watch?
  case DELETED:
    // the node has been deleted by someone else
  case DATA_CHANGED:
    // someone just touched my cheese
  case CHILD_CHANGED:
    // children were changed
  default:
    // unexpected here
  }
}//end watch

Election in a Cluster

Perfect ZooKeeper provides a convenient way of choosing a leader / master from a cluster by calling method elect():

let (me, leader, candidates) = try z.elect("/path/to")

If success, the return value of elect() function is a tuple of (me: Int, leader: Int, candidates: [Int]), which means every candidate, include the current instance, will be included in the candidates array as an integer. Result me is the current instance's election serial number and the result of leader is the number represents the final leader in this election. If me == leader, then congratulations - the current instance of ZooKeeper just won the election. In such a case, further actions should be done to upgrade current instance into the master of cluster.

ACL Operations

ACL, the access control list, can be manipulated in ZooKeeper API by core data structure ACL_vector, i.e., a C based pointer array. Content of such a structure could be checked by similar operations as below:

func show(_ aclArray: ACL_vector) {
  guard let pAcl = aclArray.data else {
    // the array doesn't contain any valid data
    return
  }
  var i = 0
  while (Int32(i) < aclArray.count) {
    let cursor = pAcl.advanced(by: i)
    let acl = cursor.pointee
    let scheme = String(cString: acl.id.scheme)
    let id = String(cString: acl.id.id)
    // id is subject to scheme
    // if scheme is "world", then id shall be "anyone",
    // scheme "auth" doesn't use any id.
    // scheme "ip" uses host ip as id, such as "1.2.3.4/5"
    // scheme "x509" uses client X500 principal as id.
    // scheme "digest" use name:password to generate MD5 as id:
    // `username:base64 encoded SHA1 password digest.`
    print("id: \(id)")
    print("scheme: \(scheme)")
    // permission is a combination of :
    // ZOO_PERM_READ | ZOO_PERM_WRITE | ZOO_PERM_CREATE
    // ZOO_PERM_DELETE | ZOO_PERM_ADMIN | ZOO_PERM_ALL
    let perm = String(format: "%8X", acl.perms)
    print("permissions: \(perm)")
    i += 1
  }
}

⚠️ CAUTION ⚠️ To manipulate ACL_vector with permission options, please make sure to import the C ZooKeeper library:

import czookeeper

Get ACL Info

Method getACL() can retrieve ACL info from a ZNode:

let (acl, stat) = try z.getACL("/path/to")

The return value is a tuple of (acl: ACL_vector, stat: Stat) stands for the acl info as an ACL_vector pointer array and status of the node.

Set ACL Info

Method func setACL(_ path: String, version: Int32 = -1, acl: ACL_vector) throws may save an ACL setting to a ZNode, where the version could be skipped by default and providing a valid ACL_vector pointer array:

var acl = ACL_vector()
// do some modification to acl variable
try z.setACL("/path/to", acl:acl)

However, there is also another alternative form of setACL() by replacing the complicated ACL_vector to preset ACL template:

// available templates include .OPEN, .READ and .CREATOR
// default is .OPEN, which means nothing to restrict
try z.setACL("/path/to", aclTemplate: .READ)