package Munin::Master::UpdateWorker;
use base qw(Munin::Master::Worker);

use warnings;
use strict;

use Carp;
use English qw(-no_match_vars);
use Log::Log4perl qw( :easy );

use File::Basename;
use File::Path;
use File::Spec;
use Munin::Master::Config;
use Munin::Master::Node;
use Munin::Master::Utils;
use RRDs;
use Time::HiRes;
use Data::Dumper;
use Scalar::Util qw(weaken);
    
use List::Util qw(max);

my $config = Munin::Master::Config->instance()->{config};

# Flags that have RRD autotuning enabled.
my $rrd_tune_flags = {
	type => '--data-source-type', 
	max => '--maximum', 
	min => '--minimum',
};

sub new {
    my ($class, $host, $worker) = @_;

    my $self = $class->SUPER::new($host->get_full_path);
    $self->{host} = $host;
    $self->{node} = Munin::Master::Node->new($host->{address},
                                             $host->{port},
                                             $host->{host_name},
					     $host);
    $self->{state} = {};
    $self->{worker} = $worker;
    weaken($self->{worker});

    return $self;
}


sub do_work {
    my ($self) = @_;

    my $update_time = Time::HiRes::time;

    my $host = $self->{host}{host_name};
    my $path = $self->{host}->get_full_path;
    $path =~ s{[:;]}{-}g;

    # Parameters are space-separated from the main address
    my ($url, $params) = split(/ +/, $self->{host}{address}, 2);
    my $uri = new URI($url);

    # If the scheme is not defined, it's a plain host.
    # Prefix it with munin:// to be able to parse it like others
    $uri = new URI("munin://" . $url) unless $uri->scheme;

    my $nodedesignation;
    if ($uri->scheme eq "ssh" || $uri->scheme eq "cmd") {
      $nodedesignation = $host." (".$self->{host}{address}.")";
    }else{
      $nodedesignation = $host." (".$self->{host}{address}.":".$self->{host}{port}.")";
    }

    my $lock_file = sprintf ('%s/munin-%s.lock',
			     $config->{rundir},
			     $path);

    if (!munin_getlock($lock_file)) {
	WARN "Could not get lock $lock_file for $nodedesignation. Skipping node.";
        die "Could not get lock $lock_file for $nodedesignation. Skipping node.\n";
    }

    # Reading the state file, no need to lock it, since it's per node and we
    # already have a lock on this.
    my $state_file = sprintf ('%s/state-%s.storable', $config->{dbdir}, $path); 
    DEBUG "[DEBUG] Reading state for $path in $state_file";
    $self->{state} = munin_read_storable($state_file) || {};

    my %all_service_configs = (
	data_source => {},
	global => {},
	);

    INFO "[INFO] starting work in $$ for $nodedesignation.\n";
    my $done = $self->{node}->do_in_session(sub {

	eval {
	    # A I/O timeout results in a violent exit.  Catch and handle.

	    my @node_capabilities = $self->{node}->negotiate_capabilities();

            # Handle spoolfetch, one call to retrieve everything
	    my %whole_config;
	    my @plugins;
	    if (grep /^spool$/, @node_capabilities) {
		    my $spoolfetch_last_timestamp = $self->get_spoolfetch_timestamp();
		    local $0 = "$0 -- spoolfetch($spoolfetch_last_timestamp)";
		    %whole_config = $self->uw_spoolfetch($spoolfetch_last_timestamp);

		    # XXX - Commented out, should be protect by a "if logger.isDebugEnabled()"
		    #       since it is quite expensive
		    #DEBUG "[DEBUG] whole_config:" . Dumper(\%whole_config);

		    # spoolfetching reported no data, skipping it.
		    if (! $whole_config{global}{multigraph}[1]) {
			    INFO "[INFO] $nodedesignation didn't send any data for spoolfetch. Ignoring it.";
			    # adding ourself to failed_workers, so we use 
			    push @{ $self->{worker}->{failed_workers} },  $self->{ID};
			   die "NO_SPOOLFETCH_DATA";
		    }

		    # Gets the plugins from spoolfetch
		    # Only keep the first one, the others will be multigraph-fetched
		    @plugins = ( $whole_config{global}{multigraph}[0] ) ;
	    }

	    # Note: A multigraph plugin can present multiple services.
	    @plugins = $self->{node}->list_plugins() unless @plugins;

	    for my $plugin (@plugins) {
		if (%{$config->{limit_services}}) {
		    next unless $config->{limit_services}{$plugin};
		}

		DEBUG "[DEBUG] for my $plugin (@plugins)";

		# Ask for config only if spoolfetch didn't already send it
		my %service_config = %whole_config;
	        unless (%service_config) {
		       local $0 = "$0 -- config($plugin)";
		       %service_config = $self->uw_fetch_service_config($plugin);
		}

		unless (%service_config) {
		    WARN "[WARNING] Service $plugin on $nodedesignation ".
			"returned no config";
		    next;
		}

		# Check if this plugin has already sent its data via a dirtyconfig
		# Note that spoolfetch also uses dirtyconfig
		my %service_data = $self->handle_dirty_config(\%service_config);
			
		# default is 0 sec : always update when asked
		my $update_rate = get_global_service_value(\%service_config, $plugin, "update_rate", 0); 
		my ($update_rate_in_seconds, $is_update_aligned) = parse_update_rate($update_rate);
		DEBUG "[DEBUG] update_rate $update_rate_in_seconds for $plugin on $nodedesignation";

		if (! %service_data) {
			# Check if this plugin has to be updated
			if ($update_rate_in_seconds 
				&& $self->is_fresh_enough($nodedesignation, $plugin, $update_rate_in_seconds)) {
			    # It's fresh enough, skip this $service
			    DEBUG "[DEBUG] $plugin is fresh enough, not updating it";
			    next;
			}

			# __root__ is only a placeholder plugin for 
			# an empty spoolfetch so we should ignore it 
			# if asked to fetch it. 
			# But we should still do everything after than.
			if ($plugin ne "__root__") {
				DEBUG "[DEBUG] No service data for $plugin, fetching it";
				local $0 = "$0 -- fetch($plugin)";
				%service_data = $self->{node}->fetch_service_data($plugin);
			}
		}

		# If update_rate is aligned, round the "when" for alignement
		if ($is_update_aligned) {
			foreach my $service (keys %service_data) {
				my $current_service_data = $service_data{$service};
				foreach my $field (keys %$current_service_data) {
					my $whens = $current_service_data->{$field}->{when};
					for (my $i = 0; $i < scalar @$whens; $i ++) {
						my $when = $whens->[$i];
						my $rounded_when = round_to_granularity($when, $update_rate_in_seconds);
						$whens->[$i] = $rounded_when;
					}
				}
			}
		}


		# Since different plugins can populate multiple
		# positions in the service namespace we'll check for
		# collisions and warn of them.

		for my $service (keys %{$service_config{data_source}}) {
		    if (defined($all_service_configs{data_source}{$service})) {
			WARN "[WARNING] Service collision: plugin $plugin on "
			    ."$nodedesignation reports $service which already "
			    ."exists on that host.  Deleting new data.";
			delete($service_config{data_source}{$service});
		    delete($service_data{$service})
			if defined $service_data{$service};
		    }
		}

		# .extinfo fields come from "fetch" but must be saved
		# like "config".

		for my $service (keys %service_data) {
		    for my $ds (keys %{$service_data{$service}}) {
			my $extinfo = $service_data{$service}{$ds}{extinfo};
			if (defined $extinfo) {
			    $service_config{data_source}{$service}{$ds}{extinfo} =
				$extinfo;
			    DEBUG "[DEBUG] Copied extinfo $extinfo into "
				."service_config for $service / $ds on "
				.$nodedesignation;
			}
		    }
		}

		$self->_compare_and_act_on_config_changes(\%service_config);

		%{$all_service_configs{data_source}} = (
		    %{$all_service_configs{data_source}},
		    %{$service_config{data_source}});

		%{$all_service_configs{global}} = (
		    %{$all_service_configs{global}},
		    %{$service_config{global}});

		my $last_updated_timestamp = $self->_update_rrd_files(\%service_config, \%service_data);
		if ($last_updated_timestamp) {
		    $self->set_spoolfetch_timestamp($last_updated_timestamp);
		}
	    } # for @plugins

	    # Send "quit" to node
	    $self->{node}->quit();
	   
	}; # eval

	# kill the remaining process if needed
	if ($self->{node}->{pid} && kill(0, $self->{node}->{pid})) {
		INFO "[INFO] Killing subprocess $self->{node}->{pid}";
		kill 'TERM', $self->{node}->{pid};
	}

	if ($EVAL_ERROR =~ m/^NO_SPOOLFETCH_DATA /) {
	    INFO "[INFO] No spoolfetch data for $nodedesignation";
	    return;
	} elsif ($EVAL_ERROR) {
	    ERROR "[ERROR] Error in node communication with $nodedesignation: "
		.$EVAL_ERROR;
	    return;
	}

	# Everything went smoothly.
	DEBUG "[DEBUG] Everything went smoothly.";
	return 1;

    }); # do_in_session

    munin_removelock($lock_file);

    # Update the state file 
    DEBUG "[DEBUG] Writing state for $path in $state_file";
    munin_write_storable($state_file, $self->{state});

    # This handles failure in do_in_session,
    return undef if ! $done || ! $done->{exit_value};

    return {
        time_used => Time::HiRes::time - $update_time,
        service_configs => \%all_service_configs,
    }
}

sub get_global_service_value {
	my ($service_config, $service, $conf_field_name, $default) = @_;
	foreach my $array (@{$service_config->{global}{$service}}) {
		my ($field_name, $field_value) = @$array;
		if ($field_name eq $conf_field_name) {
			return $field_value;
		}
	}

	return $default;
}

sub is_fresh_enough {
	my ($self, $nodedesignation, $service, $update_rate_in_seconds) = @_;

	DEBUG "is_fresh_enough asked for $service with a rate of $update_rate_in_seconds";

	my $last_updated = $self->{state}{last_updated}{$service} || "0 0";
	DEBUG "last_updated{$service}: " . $last_updated;
	my @last = split(/ /, $last_updated);
   
	use Time::HiRes qw(gettimeofday tv_interval);	
	my $now = [ gettimeofday ];

	my $age = tv_interval(\@last, $now); 	
	DEBUG "last: [" . join(",", @last) . "], now: [" . join(", ", @$now) . "], age: $age";
	my $is_fresh_enough = ($age < $update_rate_in_seconds) ? 1 : 0;
	DEBUG "is_fresh_enough  $is_fresh_enough";

	if (! $is_fresh_enough) {
		DEBUG "new value: " . join(" ", @$now);
		$self->{state}{last_updated}{$service} = join(" ", @$now);
	}

	return $is_fresh_enough;
}

sub get_spoolfetch_timestamp {
	my ($self) = @_;

	my $last_updated_value = $self->{state}{spoolfetch} || "0";
	return $last_updated_value;
}

sub set_spoolfetch_timestamp {
	my ($self, $timestamp) = @_;
	DEBUG "[DEBUG] set_spoolfetch_timestamp($timestamp)";

	# Using the last timestamp sended by the server :
	# -> It can be be different than "now" to be able to process the backlock slowly
	$self->{state}{spoolfetch} = $timestamp;
}

sub parse_update_rate {
	my ($update_rate_config) = @_;

	my ($is_update_aligned, $update_rate_in_sec);
	if ($update_rate_config =~ m/(\d+[a-z]?)( aligned)?/) {
		$update_rate_in_sec = to_sec($1);
		$is_update_aligned = $2;
	} else {
		return (0, 0);
	}

	return ($update_rate_in_sec, $is_update_aligned);
}

sub round_to_granularity {
	my ($when, $granularity_in_sec) = @_;
	$when = time if ($when eq "N"); # N means "now"

	my $rounded_when = $when - ($when % $granularity_in_sec);
	return $rounded_when;
}

sub handle_dirty_config {
	my ($self, $service_config) = @_;
	
	my %service_data;

	my $services = $service_config->{global}{multigraph};
	foreach my $service (@$services) {
		my $service_data_source = $service_config->{data_source}->{$service};
		foreach my $field (keys %$service_data_source) {
			my $field_value = $service_data_source->{$field}->{value};
			my $field_when = $service_data_source->{$field}->{when};

			# If value not present, this field is not dirty fetched
			next if (! defined $field_value);

			DEBUG "[DEBUG] handle_dirty_config:$service, $field, @$field_when";
			# Moves the "value" to the service_data
			$service_data{$service}->{$field} ||= { when => [], value => [], };
	                push @{$service_data{$service}{$field}{value}}, @$field_value;
			push @{$service_data{$service}{$field}{when}}, @$field_when;

			delete($service_data_source->{$field}{value});
			delete($service_data_source->{$field}{when});
		}
	}

	return %service_data;
}


sub uw_spoolfetch {
    my ($self, $timestamp) = @_;

    my %whole_config = $self->{node}->spoolfetch($timestamp);

    # munin.conf might override stuff
    foreach my $plugin (keys %whole_config) {
	    my $merged_config = $self->uw_override_with_conf($plugin, $whole_config{$plugin});
	    $whole_config{$plugin} = $merged_config;
    }

    return %whole_config;
}

sub uw_fetch_service_config {
    my ($self, $plugin) = @_;

    # Note, this can die for several reasons.  Caller must eval us.
    my %service_config = $self->{node}->fetch_service_config($plugin);
    my $merged_config = $self->uw_override_with_conf($plugin, \%service_config);

    return %$merged_config;
}

sub uw_override_with_conf {
    my ($self, $plugin, $service_config) = @_;

    if ($self->{host}{service_config} && 
	$self->{host}{service_config}{$plugin}) {

        my %merged_config = (%$service_config, %{$self->{host}{service_config}{$plugin}});
	$service_config = \%merged_config;
    }

    return $service_config;
}


sub _compare_and_act_on_config_changes {
    my ($self, $nested_service_config) = @_;

    # Kjellm: Why do we need to tune RRD files after upgrade?
    # Shouldn't we create a upgrade script or something instead?
    #
    # janl: Upgrade script sucks.  This way it's inline in munin and
    #  no need to remember anything or anything.

    my $just_upgraded = 0;

    my $old_config = Munin::Master::Config->instance()->{oldconfig};

    if (not defined $old_config->{version}
        or ($old_config->{version}
            ne $Munin::Common::Defaults::MUNIN_VERSION)) {
        $just_upgraded = 1;
    }

    for my $service (keys %{$nested_service_config->{data_source}}) {

        my $service_config = $nested_service_config->{data_source}{$service};

	for my $data_source (keys %{$service_config}) {
	    my $old_data_source = $data_source;
	    my $ds_config = $service_config->{$data_source};

	    my $group = $self->{host}{group}{group_name};
	    my $host = $self->{host}{host_name};

	    my $old_host_config = $old_config->{groups}{$group}{hosts}{$host};
	    my $old_ds_config = undef;

	    if ($old_host_config) {
		$old_ds_config =
		    $old_host_config->get_canned_ds_config($service,
							   $data_source);
	    }

	    if (defined($old_ds_config)
		and %$old_ds_config
		and defined($ds_config->{oldname})
		and $ds_config->{oldname}) {

		$old_data_source = $ds_config->{oldname};
		$old_ds_config =
		    $old_host_config->get_canned_ds_config($service,
							   $old_data_source);
	    }

	    if (defined($old_ds_config)
		and %$old_ds_config
		and not $self->_ds_config_eq($old_ds_config, $ds_config)) {
		$self->_ensure_filename($service,
					$old_data_source, $data_source,
					$old_ds_config, $ds_config)
		    and $self->_ensure_tuning($service, $data_source,
					      $ds_config);
		# _ensure_filename prints helpfull warnings in the log
	    } elsif ($just_upgraded) {
		$self->_ensure_tuning($service, $data_source,
				      $ds_config);
	    }
	}
    }
}


sub _ds_config_eq {
    my ($self, $old_ds_config, $ds_config) = @_;

    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
    $old_ds_config = $self->_get_rrd_data_source_with_defaults($old_ds_config);

    # We only compare keys that are autotuned to avoid needless RRD tuning,
    # since RRD tuning is bad for perf (flush rrdcached)
    for my $key (keys %$rrd_tune_flags) {
	my $old_value = $old_ds_config->{$key};
	my $value = $ds_config->{$key};

        # if both keys undefined, look no further
        next unless (defined($old_value) || defined($value));

	# so, at least one of the 2 is defined

	# False if the $old_value is not defined
	return 0 unless (defined($old_value));

	# if something isn't the same, return false
        return 0 if (! defined $value || $old_value ne $value);
    }

    # Nothing different found, it has to be equal.
    return 1;
}


sub _ensure_filename {
    my ($self, $service, $old_data_source, $data_source,
        $old_ds_config, $ds_config) = @_;

    my $rrd_file = $self->_get_rrd_file_name($service, $data_source,
                                             $ds_config);
    my $old_rrd_file = $self->_get_rrd_file_name($service, $old_data_source,
                                                 $old_ds_config);

    my $hostspec = $self->{node}{host}.'/'.$self->{node}{address}.':'.
	$self->{node}{port};

    if ($rrd_file ne $old_rrd_file) {
        if (-f $old_rrd_file and -f $rrd_file) {
            my $host = $self->{host}{host_name};
            WARN "[WARNING]: $hostspec $service $data_source config change "
		. "suggests moving '$old_rrd_file' to '$rrd_file' "
		. "but both exist; manually merge the data "
                . "or remove whichever file you care less about.\n";
	    return '';
        } elsif (-f $old_rrd_file) {
            INFO "[INFO]: Config update, changing name of '$old_rrd_file'"
                   . " to '$rrd_file' on $hostspec ";
            unless (rename ($old_rrd_file, $rrd_file)) {
                ERROR "[ERROR]: Could not rename '$old_rrd_file' to"
		    . " '$rrd_file' for $hostspec: $!\n";
                return '';
            }
        }
    }

    return 1;
}


sub _ensure_tuning {
    my ($self, $service, $data_source, $ds_config) = @_;
    my $success = 1;

    my $rrd_file =
        $self->_get_rrd_file_name($service, $data_source,
                                  $ds_config);

    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
    for my $rrd_prop (keys %$rrd_tune_flags) {
        INFO "[INFO]: Config update, ensuring $rrd_prop of"
	    . " '$rrd_file' is '$ds_config->{$rrd_prop}'.\n";
        RRDs::tune($rrd_file, $rrd_tune_flags->{$rrd_prop},
                   "42:$ds_config->{$rrd_prop}");
        if (my $tune_error = RRDs::error()) {
            ERROR "[ERROR] Tuning $rrd_prop of '$rrd_file' to"
		. " '$ds_config->{$rrd_prop}' failed.\n";
            $success = '';
        }
    }

    return $success;
}


sub _update_rrd_files {
    my ($self, $nested_service_config, $nested_service_data) = @_;

    my $nodedesignation = $self->{host}{host_name}."/".
	$self->{host}{address}.":".$self->{host}{port};

    my $last_timestamp =
    	max(0,
    	    map {
    		my $svc = $_;
    		map {
    		    my $ds = $_;
    		    @{$nested_service_data->{$svc}->{$ds}->{when} || []};
    		} keys %{$nested_service_config->{data_source}{$svc}};
    	    } keys %{$nested_service_config->{data_source}}
    	);
    my $last_timestamp_or_now = ($last_timestamp > 0) ? $last_timestamp : time;

    for my $service (keys %{$nested_service_config->{data_source}}) {

	my $service_config = $nested_service_config->{data_source}{$service};
	my $service_data   = $nested_service_data->{$service};

	for my $ds_name (keys %{$service_config}) {
	    my $ds_config = $service_config->{$ds_name};

	    unless (defined($ds_config->{label})) {
		ERROR "[ERROR] Unable to update $service on $nodedesignation -> $ds_name: Missing data source configuration attribute: label";
		next;
	    }
	    
	    # Sets the DS resolution, searching in that order : 
	    # - per field 
	    # - per plugin
	    # - globally
            my $configref = $self->{node}{configref};
	    $ds_config->{graph_data_size} ||= get_config_for_service($nested_service_config->{global}{$service}, "graph_data_size");
	    $ds_config->{graph_data_size} ||= $config->{graph_data_size};
            
	    $ds_config->{update_rate} ||= get_config_for_service($nested_service_config->{global}{$service}, "update_rate");
	    $ds_config->{update_rate} ||= $config->{update_rate};
	    $ds_config->{update_rate} ||= 300; # default is 5 min

	    DEBUG "[DEBUG] asking for a rrd of size : " . $ds_config->{graph_data_size};

	    # Avoid autovivification (for multigraphs)
	    my $first_epoch = (defined($service_data) and defined($service_data->{$ds_name})) ? ($service_data->{$ds_name}->{when}->[0]) : 0;
	    my $rrd_file = $self->_create_rrd_file_if_needed($service, $ds_name, $ds_config, $first_epoch);

	    if (defined($service_data) and defined($service_data->{$ds_name})) {
		$self->_update_rrd_file($rrd_file, $ds_name, $service_data->{$ds_name}, $last_timestamp_or_now);
	    }
	    else {
		WARN "[WARNING] Service $service on $nodedesignation returned no data for label $ds_name";
	    }
	}
    }

    return $last_timestamp;
}

sub get_config_for_service {
	my ($array, $key) = @_;
	
	for my $elem (@$array) {
		next unless $elem->[0] && $elem->[0] eq $key;
		return $elem->[1];
	}

	# Not found
	return undef;
}


sub _get_rrd_data_source_with_defaults {
    my ($self, $data_source) = @_;

    # Copy it into a new hash, we don't want to alter the $data_source
    # and anything already defined should not be overridden by defaults
    my $ds_with_defaults = {
	    type => 'GAUGE',
	    min => 'U',
	    max => 'U',

	    update_rate => 300,
	    graph_data_size => 'normal',
    };
    for my $key (keys %$data_source) {
	    $ds_with_defaults->{$key} = $data_source->{$key};
    }

    return $ds_with_defaults;
}


sub _create_rrd_file_if_needed {
    my ($self, $service, $ds_name, $ds_config, $first_epoch) = @_;

    my $rrd_file = $self->_get_rrd_file_name($service, $ds_name, $ds_config);
    unless (-f $rrd_file) {
        $self->_create_rrd_file($rrd_file, $service, $ds_name, $ds_config, $first_epoch);
    }

    return $rrd_file;
}


sub _get_rrd_file_name {
    my ($self, $service, $ds_name, $ds_config) = @_;
    
    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
    my $type_id = lc(substr(($ds_config->{type}), 0, 1));

    my $path = $self->{host}->get_full_path;
    $path =~ s{[;:]}{/}g;

    # Multigraph/nested services will have . in the service name in this function.
    $service =~ s{\.}{-}g;

    # The following is rigged to match the corresponding function in
    # munin-graph/munin-html where it's less clear what are groups and
    # what are hosts and what are services, and they simply pop
    # elements off the end and so on.

    my $file = sprintf("%s-%s-%s-%s.rrd",
                       $path,
                       $service,
                       $ds_name,
                       $type_id);

    $file = File::Spec->catfile($config->{dbdir}, 
				$file);
	
    DEBUG "[DEBUG] rrd filename: $file\n";

    return $file;
}


sub _create_rrd_file {
    my ($self, $rrd_file, $service, $ds_name, $ds_config, $first_epoch) = @_;

    INFO "[INFO] creating rrd-file for $service->$ds_name: '$rrd_file'";

    munin_mkdir_p(dirname($rrd_file), oct(777));

    my @args;

    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
    my $resolution = $ds_config->{graph_data_size};
    my $update_rate = $ds_config->{update_rate};
    if ($resolution eq 'normal') {
	$update_rate = 300; # 'normal' means hard coded RRD $update_rate
        push (@args,
              "RRA:AVERAGE:0.5:1:576",   # resolution 5 minutes
              "RRA:MIN:0.5:1:576",
              "RRA:MAX:0.5:1:576",
              "RRA:AVERAGE:0.5:6:432",   # 9 days, resolution 30 minutes
              "RRA:MIN:0.5:6:432",
              "RRA:MAX:0.5:6:432",
              "RRA:AVERAGE:0.5:24:540",  # 45 days, resolution 2 hours
              "RRA:MIN:0.5:24:540",
              "RRA:MAX:0.5:24:540",
              "RRA:AVERAGE:0.5:288:450", # 450 days, resolution 1 day
              "RRA:MIN:0.5:288:450",
              "RRA:MAX:0.5:288:450");
    } 
    elsif ($resolution eq 'huge') {
	$update_rate = 300; # 'huge' means hard coded RRD $update_rate
        push (@args, 
              "RRA:AVERAGE:0.5:1:115200",  # resolution 5 minutes, for 400 days
              "RRA:MIN:0.5:1:115200",
              "RRA:MAX:0.5:1:115200"); 
    } elsif ($resolution =~ /^custom (.+)/) {
        # Parsing resolution to achieve computer format as defined on the RFC :
        # FULL_NB, MULTIPLIER_1 MULTIPLIER_1_NB, ... MULTIPLIER_NMULTIPLIER_N_NB 
        my @resolutions_computer = parse_custom_resolution($1, $update_rate);
        foreach my $resolution_computer(@resolutions_computer) {
            my ($multiplier, $multiplier_nb) = @{$resolution_computer};
	    # Always add 10% to the RRA size, as specified in 
	    # http://munin-monitoring.org/wiki/format-graph_data_size
	    $multiplier_nb += int ($multiplier_nb / 10) || 1;
            push (@args, 
                "RRA:AVERAGE:0.5:$multiplier:$multiplier_nb",
                "RRA:MIN:0.5:$multiplier:$multiplier_nb",
                "RRA:MAX:0.5:$multiplier:$multiplier_nb"
            ); 
        }
    }

    # Add the RRD::create prefix (filename & RRD params) 
    my $heartbeat = $update_rate * 2;
    unshift (@args,
        $rrd_file,
        "--start", ($first_epoch - $update_rate),
	"-s", $update_rate,
        sprintf('DS:42:%s:%s:%s:%s', 
                $ds_config->{type}, $heartbeat, $ds_config->{min}, $ds_config->{max}),
    );

    DEBUG "[DEBUG] RRDs::create @args";
    RRDs::create @args;
    if (my $ERROR = RRDs::error) {
        ERROR "[ERROR] Unable to create '$rrd_file': $ERROR";
    }
}

sub parse_custom_resolution {
	my @elems = split(',\s*', shift);
	my $update_rate = shift;

	DEBUG "[DEBUG] update_rate: $update_rate";

        my @computer_format;

	# First element is always the full resolution
	my $full_res = shift @elems;
	if ($full_res =~ m/^\d+$/) {
		# Only numeric, computer format
		unshift @elems, "1 $full_res";
	} else {
		# Human readable. Adding $update_rate in front of
		unshift @elems, "$update_rate for $full_res";
	}

        foreach my $elem (@elems) {
                if ($elem =~ m/(\d+) (\d+)/) {
                        # nothing to do, already in computer format
                        push @computer_format, [$1, $2];
                } elsif ($elem =~ m/(\w+) for (\w+)/) {
                        my $nb_sec = to_sec($1);
                        my $for_sec = to_sec($2);
                        
			my $multiplier = int ($nb_sec / $update_rate);
                        my $multiplier_nb = int ($for_sec / $nb_sec);

			DEBUG "[DEBUG] $elem"
				. " -> nb_sec:$nb_sec, for_sec:$for_sec"
				. " -> multiplier:$multiplier, multiplier_nb:$multiplier_nb"
			;
                        push @computer_format, [$multiplier, $multiplier_nb];
                }
	}

        return @computer_format;
}

# return the number of seconds 
# for the human readable format
# s : second,  m : minute, h : hour
# d : day, w : week, t : month, y : year
sub to_sec {
	my $secs_table = {
		"s" => 1,
		"m" => 60,
		"h" => 60 * 60,
		"d" => 60 * 60 * 24,
		"w" => 60 * 60 * 24 * 7,
		"t" => 60 * 60 * 24 * 31, # a month always has 31 days
		"y" => 60 * 60 * 24 * 365, # a year always has 365 days 
	};

	my ($target) = @_;
	if ($target =~ m/(\d+)([smhdwty])/i) {
		return $1 * $secs_table->{$2};	
	} else {
		# no recognised unit, return the int value as seconds
		return int $target;
	}
}

sub to_mul {
	my ($base, $target) = @_;
	my $target_sec = to_sec($target);
	if ($target %% $base != 0) {
		return 0;
	}

	return round($target / $base); 
}

sub to_mul_nb {
	my ($base, $target) = @_;
	my $target_sec = to_sec($target);
	if ($target %% $base != 0) {
		return 0;
	}
}

sub _update_rrd_file {
    my ($self, $rrd_file, $ds_name, $ds_values, $max_timestamp) = @_;

    my $values = $ds_values->{value};

    # Some kind of mismatch between fetch and config can cause this.
    return if !defined($values);  

    my ($previous_updated_timestamp, $previous_updated_value) = @{ $self->{state}{value}{"$rrd_file:42"}{current} || [ ] };
    my @update_rrd_data;
	if ($config->{"rrdcached_socket"}) {
		if (! -e $config->{"rrdcached_socket"} || ! -w $config->{"rrdcached_socket"}) {
			WARN "[WARN] RRDCached feature ignored: rrdcached socket not writable";
		} elsif($RRDs::VERSION < 1.3){
			WARN "[WARN] RRDCached feature ignored: perl RRDs lib version must be at least 1.3. Version found: " . $RRDs::VERSION;
		} else {
			# Using the RRDCACHED_ADDRESS environnement variable, as
			# it is way less intrusive than the command line args.
			$ENV{RRDCACHED_ADDRESS} = $config->{"rrdcached_socket"};
		}
	} 
    
    my ($current_updated_timestamp, $current_updated_value) = ($previous_updated_timestamp, $previous_updated_value);
    for (my $i = 0; $i < scalar @$values; $i++) { 
        my $value = $values->[$i];
        my $when = $ds_values->{when}[$i];

	if ($when == $self->{node}->NO_TIMESTAMP) {
	    $when = $max_timestamp;
	}

	# Ignore values that is not in monotonic increasing timestamp for the RRD.
	# Otherwise it will reject the whole update
	next if ($current_updated_timestamp && $when <= $current_updated_timestamp);

        if ($value =~ /\d[Ee]([+-]?\d+)$/) {
            # Looks like scientific format.  RRDtool does not
            # like it so we convert it.
            my $magnitude = $1;
            if ($magnitude < 0) {
                # Preserve at least 4 significant digits
                $magnitude = abs($magnitude) + 4;
                $value = sprintf("%.*f", $magnitude, $value);
            } else {
                $value = sprintf("%.4f", $value);
            }
        }
        
        # Schedule for addition
        push @update_rrd_data, "$when:$value";

	$current_updated_timestamp = $when;
	$current_updated_value = $value;
    }

    DEBUG "[DEBUG] Updating $rrd_file with @update_rrd_data";
    if ($ENV{RRDCACHED_ADDRESS} && (scalar @update_rrd_data > 32) ) {
        # RRDCACHED only takes about 4K worth of commands. If the commands is
        # too large, we have to break it in smaller calls.
        #
        # Note that 32 is just an arbitrary choosed number. It might be tweaked.
        #
        # For simplicity we only call it with 1 update each time, as RRDCACHED
        # will buffer for us as suggested on the rrd mailing-list.
        # https://lists.oetiker.ch/pipermail/rrd-users/2011-October/018196.html
        for my $update_rrd_data (@update_rrd_data) {
            RRDs::update($rrd_file, $update_rrd_data);
            # Break on error.
            last if RRDs::error;
        }
    } else {
        RRDs::update($rrd_file, @update_rrd_data);
    }

    if (my $ERROR = RRDs::error) {
        #confess Dumper @_;
        ERROR "[ERROR] In RRD: Error updating $rrd_file: $ERROR";
    }

    # Stores the previous and the current value in the state db to avoid having to do an RRD lookup if needed
    $self->{state}{value}{"$rrd_file:42"}{current} = [ $current_updated_timestamp, $current_updated_value ]; 
    $self->{state}{value}{"$rrd_file:42"}{previous} = [ $previous_updated_timestamp, $previous_updated_value ]; 

    return scalar @update_rrd_data;
}

sub dump_to_file
{
	my ($filename, $obj) = @_;
	open(DUMPFILE, ">> $filename");

	use Data::Dumper;
	print DUMPFILE Dumper($obj);

	close(DUMPFILE);
}

1;


__END__

=head1 NAME

Munin::Master::UpdateWorker - FIX

=head1 SYNOPSIS

FIX

=head1 METHODS

=over

=item B<new>

FIX

=item B<do_work>

FIX

=back

=head1 COPYING

Copyright (C) 2002-2009  Jimmy Olsen, et al.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; version 2 dated June, 1991.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along
  with this program; if not, write to the Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.


