Skip to content

Instantly share code, notes, and snippets.

@porst17
Forked from malex984/heartbeat2.py
Last active August 30, 2016 10:53
Show Gist options
  • Save porst17/1b2076e6d1baf4375500 to your computer and use it in GitHub Desktop.
Save porst17/1b2076e6d1baf4375500 to your computer and use it in GitHub Desktop.
Heartbeat server / client and libraries
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h2>Open your JavaScript console!</h2>
<div>
<form name="my_form">
App ID: <input type="text" name="app_id" value="browser_app"/><br/>
Interval: <input type="text" name="interval" value="2"/> seconds
</form>
</div>
<div>
<button type="button" onclick="heartbeart( document.my_form.app_id.value, 'hb_init', document.my_form.interval.value );">Init HB</button>
<button type="button" onclick="heartbeart( document.my_form.app_id.value, 'hb_ping', document.my_form.interval.value );">Ping HB</button>
<button type="button" onclick="heartbeart( document.my_form.app_id.value, 'hb_done', document.my_form.interval.value );">Done HB</button>
</div>
<div>
<textarea id="console" style="width:100%; height: 400px;"></textarea>
</div>
<script>
var HB_URL = "http://localhost:8888";
var text_console = document.getElementById( "console" );
text_console.value = "";
function heartbeart( APP_ID, HB_COMMAND, HB_INTERVAL )
{
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
text_console.value += "RESPONSE: " + xhttp.responseText + "\n";
}
};
var url = HB_URL + "/" + HB_COMMAND + "?" + HB_INTERVAL + "&appid=" + APP_ID + "&cache_buster=" + new Date().getTime()
text_console.value += " REQUEST: " + url + "\n";
xhttp.open("GET", url, true);
xhttp.send();
}
</script>
</body>
</html>
/*
* Copyright 2016 Christian Stussak
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
//begin private closure
// Libraries and classes in JS: http://frugalcoder.us/post/2010/02/11/js-classes.aspx
// Static members are denoted by a leading '_'.
// The documentation is supposed to be JSDOC 3.
// Online JSDOC generator: http://jsdoc.sanstv.ru/
// BEGIN - private static members
var _initCommand = 'hb_init';
var _pingCommand = 'hb_ping';
var _doneCommand = 'hb_done';
var _onLoadCalled = false;
window.addEventListener("load", function() {
_onLoadCalled = true;
}, false);
/**
* Sends a HTTP request to the heartbeat server.
* @private
* @static
*/
function _heartbeat(appId, url, command, interval, async, debugLog) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
debugLog("RESPONSE: " + xhttp.responseText);
}
};
var fullUrl = url + "/" + command + "?" + interval + "&appid=" + appId + "&cache_buster=" + new Date().getTime()
debugLog("REQUEST: " + fullUrl);
xhttp.open("GET", fullUrl, async);
xhttp.send();
return xhttp;
}
var _defaultUrl = 'http://localhost:8888';
var _defaultAppId = 'browser_app';
/**
* Get URL parameter.
* @private
* @static
* @see https://stackoverflow.com/questions/11582512/how-to-get-url-parameters-with-javascript/11582513
*/
function _getUrlParameter(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
}
var _passedUrl = _getUrlParameter('HB_URL');
var _passedAppId = _getUrlParameter('HB_APP_ID');
// END - private static members
/**
* This callback is used to log debug messages.
* @callback Heartbeat~debugLog
* @param {String} message
*/
// BEGIN - constructor
/**
* Creates an instance of the Heartbeat class.
* @public
* @class Heartbeat
* @classdesc This class provides functionality to send regular heartbeat
* signals to a HTTP server. Once initialized, there is usually
* no need to take further action except for changing the
* interval between subsequent heartbeats, see {@link Heartbeat#setInterval}.
* @param {Object} options - JSON object containing initial heartbeat settings.
* @param {String} [option.url] - The base URL to use when sending the heartbeat.
* @param {String} [option.appId] - The application id that is send with each heartbeat.
* @param {Number} [option.interval=1000] - The interval between two heartbeat pings in milliseconds.
* @param {Boolean} [option.sendInitCommand=false] - If the heartbeat init command should be send. If true, it will be send on the {@link window.onload} event or immediately if {@link window.onload} has already been triggered.
* @param {Boolean} [option.sendDoneCommand=false] - If the heartbeat done command should be send. If true, it will be send on the {@link window.onunload} event.
* @param {Heartbeat~debugLog} [option.debugLog=function(msg) {}] Callback function for debug messages.
* @example
* // Init and use internal defaults.
* var heartbeat = new Heartbeat({});
* @example
* // Init and properly set base URL and application id with partial fallback.
* var heartbeat = new Heartbeat({
* url: Heartbeat.getPassedUrl(),
* appId: Heartbeat.getPassedAppId('test_app')
* });
* @example
* // Full initialization.
* var heartbeat = new Heartbeat({
* url: Heartbeat.getPassedUrl('http://localhost:8881'),
* appId: Heartbeat.getPassedAppId('test_app'),
* interval: 5000,
* sendInitCommand: true,
* sendDoneCommand: true,
* debugLog: function(msg) {
* console.log(msg);
* }
* });
*/
this.Heartbeat = function(options) {
//if the function is called directly, return an instance of Heartbeat
if (!(this instanceof Heartbeat))
return new Heartbeat(options);
var that = this;
this.url = _defaultUrl;
this.appId = _defaultAppId;
this.interval = 5000;
this.currentHbXhr = new XMLHttpRequest();
this.debug = false;
this.timeout = null;
this.ping = null;
this.debugLog = function() {};
//handle the options initialization here
if (options.hasOwnProperty("debugLog")) {
this.debugLog = options.debugLog;
}
if (options.hasOwnProperty("url")) {
this.url = options.url;
}
if (options.hasOwnProperty("appId")) {
this.appId = options.appId;
}
if (options.hasOwnProperty("interval")) {
this.interval = options.interval;
}
if (options.hasOwnProperty("sendDoneCommand") && options.sendDoneCommand) {
var done = function() {
that.debugLog("send heartbeat done");
window.clearInterval(that.timeout);
// Abort current asynchronous heartbeat, if threre is one.
// Otherwise the currently running heartbeat request may be
// received by server *after* the synchronous `hb_done` heartbeat,
// i.e. the heartbeat events seem to be out of order.
that.currentHbXhr.abort();
that.currentHbXhr = _heartbeat(that.appId, that.url, _doneCommand, 0, false, that.debugLog);
};
window.addEventListener("unload", done, false);
}
this.ping = function() {
that.debugLog("send heartbeat ping");
that.currentHbXhr = _heartbeat(that.appId, that.url, _pingCommand, that.interval, true, that.debugLog);
}
var init = function() {
that.debugLog("init heartbeat library");
if (options.hasOwnProperty("sendInitCommand") && options.sendInitCommand) {
that.debugLog("send heartbeat init");
that.currentHbXhr = _heartbeat(that.appId, that.url, _initCommand, that.interval, true, that.debugLog);
}
that.setInterval(that.interval);
}
if (_onLoadCalled) {
init();
} else {
window.addEventListener("load", init, false);
}
}
// END - constructor
// BEGIN - public members
/**
* Get the current interval between two heartbeat pings in milliseconds.
* @public
* @returns {Number}
*/
this.Heartbeat.prototype.getInterval = function() {
return this.interval;
}
/**
* Change the interval between two heartbeat pings.
* This function sends out a heartbeat ping immediately to satisfy the
* promise of the previous heartbeat. The next heartbeat will then be send
* out after the specified number of milliseconds.
* @public
* @param {Number} newInterval - Interval between future heartbeat pings.
*/
this.Heartbeat.prototype.setInterval = function(newInterval) {
this.debugLog("set heartbeat interval to " + newInterval + "ms");
window.clearInterval(this.timeout);
this.interval = newInterval;
this.ping();
this.timeout = setInterval(this.ping, this.interval);
}
/**
* Get the base URL that is used for each heartbeat command.
* @public
* @returns {Number}
*/
this.Heartbeat.prototype.getUrl = function() {
return this.url;
}
/**
* Get the application identifier that is send with each heartbet command.
* @public
* @returns {String}
*/
this.Heartbeat.prototype.getAppId = function() {
return this.appId;
}
/**
* Get the function that is used for debug logging.
* @public
* @returns {Heartbeat~debugLog}
*/
this.Heartbeat.prototype.getDebugLog = function() {
return this.debugLog;
}
/**
* Set the function that is used for debug logging.
* @public
* @param {Heartbeat~debugLog} debugLog
*/
this.Heartbeat.prototype.setDebugLog = function(debugLog) {
this.debugLog == debugLog;
}
// END - public members
// BEGIN - public static members
/**
* Get the base URL that is passed as URL parameter HB_URL to this site.
* If HB_URL is not set, {@link fallbackUrl} is used. If {@link fallbackUrl}
* is not provided, an internal default will be returned.
* @public
* @param {String} [fallbackUrl] - Fallback that is used if HB_URL is not set.
* @returns {String}
*/
this.Heartbeat.getPassedUrl = function(fallbackUrl) {
return (_passedUrl == null) ? (typeof fallbackUrl === 'undefined' ? _defaultUrl : fallbackUrl) : _passedUrl;
}
/**
* Get the application id that is passed as URL parameter HB_APP_ID to this site.
* If HB_APP_ID is not set, {@link fallbackAppId} is used. If {@link fallbackAppId}
* is not provided, an internal default will be returned.
* @public
* @param {String} [fallbackAppId] - Fallback that is used if HB_APP_ID is not set.
* @returns {String}
*/
this.Heartbeat.getPassedAppId = function(fallbackAppId) {
return (_passedAppId == null) ? (typeof fallbackAppId === 'undefined' ? _defaultAppId : fallbackAppId) : _passedAppId;
}
// END - public static members
// end private closure then run the closure, localized to window
// (this allows to easily encapsulate this class to a namespace if necessary)
}).call(window);
#!/usr/bin/python
# import sched, time
from time import time
from time import sleep
from random import randint
from threading import Timer
from urllib2 import urlopen
import sys # what?
import urllib # what? why?
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
# https://docs.python.org/2/library/sched.html
#### SH = sched.scheduler(time, sleep)
PORT_NUMBER = 8888
HOST_NAME = 'localhost'
# localhost:8888/hb_init?48&appid=test_client_python =>
# /hb_init
# [*]
# ['48', 'appid=test_client_python']
# Accept-Encoding: identity
# Host: localhost:8080
# Connection: close
# User-Agent: Python-urllib/2.7
#
# ID: Python-urllib/2.7@127.0.0.1
visits = {}
# overdue = 0 # Just for now... TODO: FIXME?: should be a part of visit data, right?!
def toolate(ID):
ts = time()
print "[", ts, "] [", ID, "]: ", visits[ID]
d = visits[ID]
# Collect statistics about ID
if d[3] > 5:
print("TOO overdue!") # TODO: early detection of overdue clients!!???
del visits[ID]
else:
visits[ID] = (d[0], d[1], Timer(d[1], toolate, [ID]), d[3] + 1)
visits[ID][2].start() # Another Chance???
class MyHandler(BaseHTTPRequestHandler):
# s.headers, s.client_address, s.command, s.path, s.request_version, s.raw_requestline
def status(s, ID):
d = visits[ID]
t = d[0]
dd = d[1]
od = d[3]
## STATUS CODE???
app = "application [" + str(ID) + "]| od="+str(od)+";1;3;0;10 delay="+str(dd)+";;; "
if od == 0:
s.wfile.write("OK - fine " + app)
if od > 0:
s.wfile.write("WARNING - somewhat late " + app)
if od > 2:
s.wfile.write("CRITICAL - somewhat late " + app)
def do_GET(s):
global visits
# global overdue
s.send_response(200)
s.send_header('Content-type','text/html')
s.send_header('Access-Control-Allow-Origin','*')
s.end_headers()
### TODO: FIXME: the following url parsing is neither failsafe nor secure! :-(
path, _, tail = s.path.partition('?')
path = urllib.unquote(path)
if path == "/list":
for k, v in visits.iteritems():
s.wfile.write(str(k) + "\n")
return
query = tail.split('&')
if path == "/status":
if tail != "":
ID = query[0].split('=')[1] # + " @ " + s.client_address[0] # " || " + s.headers['User-Agent']
if ID in visits:
s.status(ID)
else:
s.wfile.write("CRITICAL - no application record for " + str(ID))
else:
if len(visits) == 1:
ID = visits.iterkeys().next()
s.status(ID)
elif len(visits) > 1:
s.wfile.write("WARNING - multiple (" + str(len(visits)) + ") applications" )
else:
s.wfile.write("UNKNOWN - no heartbeats yes!" )
return
# PARSING: s.path -->>> path ? T & appid = ID
T = int(query[0])
ID = query[1].split('=')[1] + " @ " + s.client_address[0] # " || " + s.headers['User-Agent']
if ID in visits:
print "PREVIOUS STATE", visits[ID]
visits[ID][2].cancel() # !
ts = time()
if path == "/hb_init":
# Hello little brother! Big Brother is watching you!
print "Creation from scratch : ", ID, " at ", ts
T = T + 1 #max(10, (T*17)/16)
visits[ID] = (ts, T, Timer(T, toolate, [ID]), 0)
s.wfile.write(T) # ?
visits[ID][2].start()
if path == "/hb_done":
print "Destruction: ", ID, " at ", ts
del visits[ID]
s.wfile.write("So Long, and Thanks for All the Fish!")
if path == "/hb_ping": #
# TODO: make sure visits[ID] exists!
print "HEART-BEAT for: ", ID, " at ", ts # Here i come again...
lastts = visits[ID][0]
lastt = visits[ID][1]
overdue = visits[ID][3]
# if (ts - lastts) > lastt: # Sorry Sir, but you are too late :-(
# overdue += 1
if overdue > 3:
print("overdue!") # TODO: early detection of overdue clients!!???
s.wfile.write("dead") # ?
# del visits[ID] #??
else:
T = T + 1 # max(3, (T*11)/8)
visits[ID] = (ts, T, Timer(T, toolate, [ID]), overdue)
s.wfile.write(T) # ?
visits[ID][2].start()
# WHAT ELSE????
return
def test_server(HandlerClass = MyHandler, ServerClass = HTTPServer, protocol="HTTP/1.0"):
"""Test the HTTP request handler class.
"""
port = PORT_NUMBER
server_address = (HOST_NAME, port)
HandlerClass.protocol_version = protocol
httpd = ServerClass(server_address, HandlerClass)
sa = httpd.socket.getsockname()
print "Serving HTTP on", sa[0], "port", sa[1], "..."
httpd.serve_forever()
def test_client():
t = randint(2, 5)
APP_ID = "test_client_python%" + str(randint(99999999, 9999999999)) # TODO: get unique ID from server?
HB_SERVER_URL = "http://" + HOST_NAME + ":" + str(PORT_NUMBER)
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read()
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read()
tt = urlopen(HB_SERVER_URL + "/hb_init?" + str(t) + "&appid="+ APP_ID ).read()
print "Initial response: ", tt
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read()
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read()
overdue = 0
i = 0
for i in xrange(1, 25):
# while True:
i = i + 1
d = randint(0, (int(t) * 2)/1)
try:
print d, " > ", tt, "?"
if d > int(tt):
overdue += 1
except:
pass
print "heart-beat: ", i, "! Promise: ", t, ", Max: ", tt, ", Delay: ", d, " sec........ overdues?: ", overdue
sleep(d)
# heartbeat:
t = randint(0, 5)
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read()
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read()
print "Ping: ", t
tt = urlopen(HB_SERVER_URL + "/hb_ping?" + str(t) + "&appid="+ APP_ID ).read()
print "Pong: ", tt
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read()
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read()
if tt == "dead": # Just for testing...
print "Ups: we run out of time..."
tt = urlopen(HB_SERVER_URL + "/hb_done?0"+ "&appid="+ APP_ID ).read()
print "Goodbye message: ", tt
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read()
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read()
break
# port = int(sys.argv[1])
if __name__ == '__main__':
print(sys.argv)
if (len(sys.argv) == 1):
test_client()
else: # Any Arguments? => Start HB Server
# if (sys.argv[1] == "-server"):
test_server()
#####################################################################
### Initial HB design:
###
### Heartbeat:
### webgl: add to render loop (initiate)
### multithread / asynchron sending (should not block the application)
### non-blocking i/o
### how often: 1 beat per second (or less)?
### TCPIP connection: leave open?
###
### GET request
### sending: add into URL "ID exhibit, heartbeat expectation" (selbstverpflichtung)
### The server knows the IP (computer).
###
### initial setup for monitoring software (also list of programs which run on which host)
### startup via Nagios?
###
### ----
### maintenance mode could be added (you put station on maintenance), if a station is not expected to be running (in order to avoid automatic restart during maintenance)
### Heartbeat protocol:
### * pass protocol parameters into container via ENVIRONMENT VARIABLES, e.g.
###
### - HEARTBEART_HTTP=http//IP:PORT/heartbeat.html?container=pong&next=%n
### = application substitutes %n with minimal time until next heartbeart (milliseconds) and sends GET request
### = server answers with maximal time for next heartbeart (>minimal heartbeat time) (otherwise it will take some action)
###
### - HEARTBEART_TCP=IP:PORT
### = CLIENT: send minimal time until next heartbeat (ms)
### = SERVER: send maximal time until next heartbeat (ms)
###
### * ENVIRONMENT PARAMETERS are passed by url parameters into browser based applications some API exposed by electron for kiosk applications?
###
### * when container is starting, the management system is waiting for some predefined time (15 seconds? same as regular waiting time when the app is running properly) before expecting the first heartbeat; afterwards the protocol is self tuning
### Heartbeat protocol implementation:
###
### * asynchronous HTTP requests in JS
### * provide a JS library the ???
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="heartbeat.js"></script>
</head>
<body>
<script>
var heartbeat = new Heartbeat({
url: Heartbeat.getPassedUrl('http://localhost:8881'),
appId: Heartbeat.getPassedAppId('test_app'),
interval: 5000,
sendInitCommand: true,
sendDoneCommand: true,
debugLog: function(msg) {
console.log(msg);
}
});
</script>
</body>
</html>
@malex984
Copy link

@malex984
Copy link

Besides, what about following https://www.nczonline.net/blog/2009/07/28/the-best-way-to-load-external-javascript/ to load this JS library?

@malex984
Copy link

Unfortunately the current python heartbeat server expects interval to be in seconds :-(
Could you please change https://gist.github.com/porst17/1b2076e6d1baf4375500#file-heartbeat-js-L46
interval to (interval/1000.0) before this JS lib gets used?

@malex984
Copy link

Any chance for getPassedAppId to ignore run-time argument HB_APP_ID???

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment