This project implements an express Swift library of ZooKeeper
This package builds with Swift Package Manager and is part of the Perfect project.
This project can only be built on
Add Perfect-ZooKeeper to your project's Package.swift:
.Package(url: "https://github.com/PerfectlySoft/Perfect-ZooKeeper.git", majorVersion: 1)
Import Perfect-ZooKeeper library to your source code:
import PerfectZooKeeper
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
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.
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)
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
}
}
"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
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:
// 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)
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)
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.
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
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)
}
Similar to save()
, the ZooKeeper load()
also has both synchronous version and asynchronous version as well:
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")
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
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
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")
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)
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`
remove()
method explicitly.
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.
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")
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
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, 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
}
}
import czookeeper
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.
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)