diff --git a/openapi.yaml b/openapi.yaml index 5586112..44aebcb 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -478,8 +478,10 @@ components: RRMSchedule: type: object properties: - cron: - type: string + crons: + type: array + items: + type: string algorithms: type: array items: diff --git a/src/main/java/com/facebook/openwifirrm/RRMSchedule.java b/src/main/java/com/facebook/openwifirrm/RRMSchedule.java index 8393677..9850a3b 100644 --- a/src/main/java/com/facebook/openwifirrm/RRMSchedule.java +++ b/src/main/java/com/facebook/openwifirrm/RRMSchedule.java @@ -19,9 +19,9 @@ public class RRMSchedule { * * This field expects a cron-like format as defined by the Quartz Job * Scheduler (CronTrigger): - * https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html + * https://www.quartz-scheduler.org/documentation/quartz-2.4.0/tutorials/crontrigger.html */ - public String cron; + public List crons; /** * The list of RRM algorithms to run. diff --git a/src/main/java/com/facebook/openwifirrm/modules/ProvMonitor.java b/src/main/java/com/facebook/openwifirrm/modules/ProvMonitor.java index 8cd14d2..9eaa726 100644 --- a/src/main/java/com/facebook/openwifirrm/modules/ProvMonitor.java +++ b/src/main/java/com/facebook/openwifirrm/modules/ProvMonitor.java @@ -8,6 +8,7 @@ package com.facebook.openwifirrm.modules; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -159,12 +160,21 @@ public class ProvMonitor implements Runnable { return null; } - RRMSchedule schedule = new RRMSchedule(); - schedule.cron = RRMScheduler + String[] crons = RRMScheduler .parseIntoQuartzCron(details.rrm.schedule); - if (schedule.cron == null || schedule.cron.isEmpty()) { + if (crons == null || crons.length == 0) { return null; } + // if ANY crons are invalid throw it out since it doesn't make sense to + // schedule partial jobs + for (String cron : crons) { + if (cron == null || cron.isEmpty()) { + return null; + } + } + + RRMSchedule schedule = new RRMSchedule(); + schedule.crons = Arrays.asList(crons); if (details.rrm.algorithms != null) { schedule.algorithms = @@ -175,6 +185,7 @@ public class ProvMonitor implements Runnable { ) .collect(Collectors.toList()); } + return schedule; } diff --git a/src/main/java/com/facebook/openwifirrm/modules/RRMScheduler.java b/src/main/java/com/facebook/openwifirrm/modules/RRMScheduler.java index 70e0725..0cbaf17 100644 --- a/src/main/java/com/facebook/openwifirrm/modules/RRMScheduler.java +++ b/src/main/java/com/facebook/openwifirrm/modules/RRMScheduler.java @@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory; import com.facebook.openwifirrm.DeviceConfig; import com.facebook.openwifirrm.DeviceDataManager; import com.facebook.openwifirrm.RRMAlgorithm; +import com.facebook.openwifirrm.RRMSchedule; import com.facebook.openwifirrm.RRMConfig.ModuleConfig.RRMSchedulerParams; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -74,15 +75,21 @@ public class RRMScheduler { /** The scheduler instance. */ private Scheduler scheduler; - /** The zones with active triggers scheduled. */ - private Set scheduledZones; + /** + * The job keys with active triggers scheduled. Job keys take the format of + * {@code :} + * + * @see #parseIntoQuartzCron(String) + * */ + private Set scheduledJobKeys; /** RRM job. */ public static class RRMJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { - String zone = context.getTrigger().getKey().getName(); + String jobKey = context.getTrigger().getKey().getName(); + String zone = jobKey.split(":")[0]; logger.debug("Executing job for zone: {}", zone); try { SchedulerContext schedulerContext = @@ -107,13 +114,14 @@ public class RRMScheduler { * @param linuxCron Linux cron with seconds * (seconds minutes hours day_of_month month day_of_week [year]) * - * @throws IllegalArgumentException when a linux cron cannot be parsed - * into a valid Quartz spec - * @return String a Quartz supported cron + * @throws IllegalArgumentException when a linux cron cannot be parsed into a + * valid Quartz spec + * @return String[] an array of length 1 or 2 of Quartz supported cron that's + * equivalent to the original linux cron */ - public static String parseIntoQuartzCron(String linuxCron) { + public static String[] parseIntoQuartzCron(String linuxCron) { if (CronExpression.isValidExpression(linuxCron)) { - return linuxCron; + return new String[] { linuxCron }; } String[] split = linuxCron.split(" "); @@ -144,15 +152,36 @@ public class RRMScheduler { // if first case failed and only day of week is *, set to ? split[DAY_OF_WEEK_INDEX] = "?"; } else { - // Quartz does not support both values being set, so return null - return null; + // Quartz does not support both values being set but the standard says that + // if both are specified then it becomes OR of the two fields. Which means + // that we can split it into two separate crons and have it work the same way + split[DAY_OF_MONTH_INDEX] = "?"; + String dayOfWeekCron = String.join(" ", split); + + split[DAY_OF_MONTH_INDEX] = dayOfMonth; + split[DAY_OF_WEEK_INDEX] = "?"; + String dayOfMonthCron = String.join(" ", split); + + if ( + !CronExpression.isValidExpression(dayOfWeekCron) || + !CronExpression.isValidExpression(dayOfMonthCron) + ) { + logger.error( + "Unable to parse cron {} into valid crons", + linuxCron + ); + return null; + } + + return new String[] { dayOfWeekCron, dayOfMonthCron }; } String quartzCron = String.join(" ", split); if (!CronExpression.isValidExpression(quartzCron)) { return null; } - return quartzCron; + + return new String[] { quartzCron }; } /** Constructor. */ @@ -194,7 +223,7 @@ public class RRMScheduler { // Schedule job and triggers scheduler.addJob(job, false); syncTriggers(); - logger.info("Scheduled {} RRM trigger(s)", scheduledZones.size()); + logger.info("Scheduled {} RRM trigger(s)", scheduledJobKeys.size()); // Start scheduler scheduler.start(); @@ -218,85 +247,98 @@ public class RRMScheduler { /** * Synchronize triggers to the current topology, adding/updating/deleting - * them as necessary. This updates {@link #scheduledZones}. + * them as necessary. This updates {@link #scheduledJobKeys}. */ public void syncTriggers() { Set scheduled = ConcurrentHashMap.newKeySet(); Set prevScheduled = new HashSet<>(); - if (scheduledZones != null) { - prevScheduled.addAll(scheduledZones); + if (scheduledJobKeys != null) { + prevScheduled.addAll(scheduledJobKeys); } // Add new triggers for (String zone : deviceDataManager.getZones()) { DeviceConfig config = deviceDataManager.getZoneConfig(zone); + RRMSchedule schedule = config.schedule; if ( - config.schedule == null || - config.schedule.cron == null || - config.schedule.cron.isEmpty() + schedule == null || schedule.crons == null || + schedule.crons.isEmpty() ) { continue; // RRM not scheduled } - try { - CronExpression.validateExpression(config.schedule.cron); - } catch (ParseException e) { - logger.error( - String.format( - "Invalid cron expression (%s) for zone %s", - config.schedule.cron, - zone - ), - e + for (int i = 0; i < schedule.crons.size(); i++) { + String cron = schedule.crons.get(i); + // if even one schedule has invalid cron, the whole thing is probably wrong + if (cron == null || cron.isEmpty()) { + logger.error("There was an invalid cron in the schedule"); + break; + } + + try { + CronExpression.validateExpression(cron); + } catch (ParseException e) { + logger.error( + String.format( + "Invalid cron expression (%s) for zone %s", + cron, + zone + ), + e + ); + continue; + } + + // Create trigger + String jobKey = String.format("%s:%d", zone, i); + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity(jobKey) + .forJob(job) + .withSchedule( + CronScheduleBuilder.cronSchedule(cron) + ) + .build(); + + try { + if (!prevScheduled.contains(jobKey)) { + scheduler.scheduleJob(trigger); + } else { + scheduler.rescheduleJob(trigger.getKey(), trigger); + } + } catch (SchedulerException e) { + logger.error( + "Failed to schedule RRM trigger for job key: " + jobKey, + e + ); + continue; + } + + scheduled.add(jobKey); + logger.debug( + "Scheduled/updated RRM for job key '{}' at: < {} >", + jobKey, + cron ); - continue; } - // Create trigger - Trigger trigger = TriggerBuilder.newTrigger() - .withIdentity(zone) - .forJob(job) - .withSchedule( - CronScheduleBuilder.cronSchedule(config.schedule.cron) - ) - .build(); - try { - if (!prevScheduled.contains(zone)) { - scheduler.scheduleJob(trigger); - } else { - scheduler.rescheduleJob(trigger.getKey(), trigger); - } - } catch (SchedulerException e) { - logger.error( - "Failed to schedule RRM trigger for zone: " + zone, - e - ); - continue; - } - scheduled.add(zone); - logger.debug( - "Scheduled/updated RRM for zone '{}' at: < {} >", - zone, - config.schedule.cron - ); } // Remove old triggers prevScheduled.removeAll(scheduled); - for (String zone : prevScheduled) { + for (String jobKey : prevScheduled) { try { - scheduler.unscheduleJob(TriggerKey.triggerKey(zone)); + scheduler.unscheduleJob(TriggerKey.triggerKey(jobKey)); } catch (SchedulerException e) { logger.error( - "Failed to remove RRM trigger for zone: " + zone, + "Failed to remove RRM trigger for jobKey: " + jobKey, e ); continue; } - logger.debug("Removed RRM trigger for zone '{}'", zone); + logger.debug("Removed RRM trigger for jobKey '{}'", jobKey); } - this.scheduledZones = scheduled; + this.scheduledJobKeys = scheduled; } /** Run RRM algorithms for the given zone. */ @@ -305,16 +347,19 @@ public class RRMScheduler { // Get algorithms from zone config DeviceConfig config = deviceDataManager.getZoneConfig(zone); - if (config.schedule == null) { + RRMSchedule schedule = config.schedule; + if (schedule == null) { logger.error("RRM schedule missing for zone '{}', aborting!", zone); return; } + if ( - config.schedule.algorithms == null || - config.schedule.algorithms.isEmpty() + schedule.algorithms == null || + schedule.algorithms.isEmpty() ) { - logger.debug("Using default RRM algorithms for zone '{}'", zone); - config.schedule.algorithms = Arrays.asList( + logger + .debug("Using default RRM algorithms for zone '{}'", zone); + schedule.algorithms = Arrays.asList( new RRMAlgorithm( RRMAlgorithm.AlgorithmType.OptimizeChannel.name() ), @@ -325,7 +370,7 @@ public class RRMScheduler { } // Execute algorithms - for (RRMAlgorithm algo : config.schedule.algorithms) { + for (RRMAlgorithm algo : schedule.algorithms) { RRMAlgorithm.AlgorithmResult result = algo.run( deviceDataManager, configManager, diff --git a/src/test/java/com/facebook/openwifirrm/modules/RRMSchedulerTest.java b/src/test/java/com/facebook/openwifirrm/modules/RRMSchedulerTest.java index 06debe2..1006dd7 100644 --- a/src/test/java/com/facebook/openwifirrm/modules/RRMSchedulerTest.java +++ b/src/test/java/com/facebook/openwifirrm/modules/RRMSchedulerTest.java @@ -9,7 +9,7 @@ package com.facebook.openwifirrm.modules; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import org.junit.jupiter.api.Test; @@ -23,48 +23,48 @@ public class RRMSchedulerTest { assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * * * *")); // correct (6 fields) - assertEquals( - "* * * ? * *", + assertArrayEquals( + new String[] { "* * * ? * *" }, RRMScheduler.parseIntoQuartzCron("* * * * * *") ); // correct (7 fields) - assertEquals( - "* * * ? * * *", + assertArrayEquals( + new String[] { "* * * ? * * *" }, RRMScheduler.parseIntoQuartzCron("* * * * * * *") ); // correct value other than * for day of month - assertEquals( - "* * * 1 * ?", + assertArrayEquals( + new String[] { "* * * 1 * ?" }, RRMScheduler.parseIntoQuartzCron("* * * 1 * *") ); - assertEquals( - "* * * 1 * ? *", + assertArrayEquals( + new String[] { "* * * 1 * ? *" }, RRMScheduler.parseIntoQuartzCron("* * * 1 * * *") ); - assertEquals( - "* * * 1/2 * ?", + assertArrayEquals( + new String[] { "* * * 1/2 * ?" }, RRMScheduler.parseIntoQuartzCron("* * * 1/2 * *") ); - assertEquals( - "* * * 1/2 * ? *", + assertArrayEquals( + new String[] { "* * * 1/2 * ? *" }, RRMScheduler.parseIntoQuartzCron("* * * 1/2 * * *") ); - assertEquals( - "* * * 1-2 * ?", + assertArrayEquals( + new String[] { "* * * 1-2 * ?" }, RRMScheduler.parseIntoQuartzCron("* * * 1-2 * *") ); - assertEquals( - "* * * 1-2 * ? *", + assertArrayEquals( + new String[] { "* * * 1-2 * ? *" }, RRMScheduler.parseIntoQuartzCron("* * * 1-2 * * *") ); - assertEquals( - "* * * 1,2 * ?", + assertArrayEquals( + new String[] { "* * * 1,2 * ?" }, RRMScheduler.parseIntoQuartzCron("* * * 1,2 * *") ); - assertEquals( - "* * * 1,2 * ? *", + assertArrayEquals( + new String[] { "* * * 1,2 * ? *" }, RRMScheduler.parseIntoQuartzCron("* * * 1,2 * * *") ); @@ -79,70 +79,70 @@ public class RRMSchedulerTest { assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * * *")); // correct value other than * for day of month - assertEquals( - "* * * ? * 1", + assertArrayEquals( + new String[] { "* * * ? * 1" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1") ); - assertEquals( - "* * * ? * 1 *", + assertArrayEquals( + new String[] { "* * * ? * 1 *" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1 *") ); - assertEquals( - "* * * ? * 1/3", + assertArrayEquals( + new String[] { "* * * ? * 1/3" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1/3") ); - assertEquals( - "* * * ? * 1/3 *", + assertArrayEquals( + new String[] { "* * * ? * 1/3 *" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1/3 *") ); - assertEquals( - "* * * ? * 1-3", + assertArrayEquals( + new String[] { "* * * ? * 1-3" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1-3") ); - assertEquals( - "* * * ? * 1-3 *", + assertArrayEquals( + new String[] { "* * * ? * 1-3 *" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1-3 *") ); - assertEquals( - "* * * ? * 1,3", + assertArrayEquals( + new String[] { "* * * ? * 1,3" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1,3") ); - assertEquals( - "* * * ? * 1,3 *", + assertArrayEquals( + new String[] { "* * * ? * 1,3 *" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1,3 *") ); // correct value other than * for day of month, make sure 0 turns into 7 - assertEquals( - "* * * ? * 7", + assertArrayEquals( + new String[] { "* * * ? * 7" }, RRMScheduler.parseIntoQuartzCron("* * * * * 0") ); - assertEquals( - "* * * ? * 7 *", + assertArrayEquals( + new String[] { "* * * ? * 7 *" }, RRMScheduler.parseIntoQuartzCron("* * * * * 0 *") ); - assertEquals( - "* * * ? * 1/7", + assertArrayEquals( + new String[] { "* * * ? * 1/7" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1/0") ); - assertEquals( - "* * * ? * 1/7 *", + assertArrayEquals( + new String[] { "* * * ? * 1/7 *" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1/0 *") ); - assertEquals( - "* * * ? * 1-7", + assertArrayEquals( + new String[] { "* * * ? * 1-7" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1-0") ); - assertEquals( - "* * * ? * 1-7 *", + assertArrayEquals( + new String[] { "* * * ? * 1-7 *" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1-0 *") ); - assertEquals( - "* * * ? * 1,7", + assertArrayEquals( + new String[] { "* * * ? * 1,7" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1,0") ); - assertEquals( - "* * * ? * 1,7 *", + assertArrayEquals( + new String[] { "* * * ? * 1,7 *" }, RRMScheduler.parseIntoQuartzCron("* * * * * 1,0 *") ); @@ -155,5 +155,119 @@ public class RRMSchedulerTest { assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7-8 *")); assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7,8")); assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7,8 *")); + + // correct value for both day of week and day of month + assertArrayEquals( + new String[] { "* * * ? * 7", "* * * 1 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1 * 0") + ); + assertArrayEquals( + new String[] { "* * * ? * 7 *", "* * * 1 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1 * 0 *") + ); + + assertArrayEquals( + new String[] { "* * * ? * 1/7", "* * * 1 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1 * 1/0") + ); + assertArrayEquals( + new String[] { "* * * ? * 1", "* * * 1/2 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1") + ); + assertArrayEquals( + new String[] { "* * * ? * 1/7", "* * * 1/2 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1/0") + ); + assertArrayEquals( + new String[] { "* * * ? * 1/7 *", "* * * 1 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1 * 1/0 *") + ); + assertArrayEquals( + new String[] { "* * * ? * 1 *", "* * * 1/2 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1 *") + ); + assertArrayEquals( + new String[] { "* * * ? * 1/7 *", "* * * 1/2 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1/0 *") + ); + + assertArrayEquals( + new String[] { "* * * ? * 1-7", "* * * 1 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1 * 1-0") + ); + assertArrayEquals( + new String[] { "* * * ? * 1", "* * * 1-3 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1") + ); + assertArrayEquals( + new String[] { "* * * ? * 1-7", "* * * 1-3 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1-0") + ); + assertArrayEquals( + new String[] { "* * * ? * 1-7 *", "* * * 1 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1 * 1-0 *") + ); + assertArrayEquals( + new String[] { "* * * ? * 1 *", "* * * 1-3 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1 *") + ); + assertArrayEquals( + new String[] { "* * * ? * 1-7 *", "* * * 1-3 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1-0 *") + ); + + assertArrayEquals( + new String[] { "* * * ? * 1-7", "* * * 1/3 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1/3 * 1-0") + ); + assertArrayEquals( + new String[] { "* * * ? * 1/7", "* * * 1-3 * ?" }, + RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1/0") + ); + assertArrayEquals( + new String[] { "* * * ? * 1-7 *", "* * * 1/3 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1/3 * 1-0 *") + ); + assertArrayEquals( + new String[] { "* * * ? * 1/7 *", "* * * 1-3 * ? *" }, + RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1/0 *") + ); + + // wrong value for either day of week or day of month + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7/8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7/8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7-8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7-8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7,8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7,8 *")); + + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7/8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7/8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7-8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7-8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7,8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7,8 *")); + + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7/8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7/8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7-8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7-8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7,8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7,8 *")); + + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7/8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7/8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7-8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7-8 *")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7,8")); + assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7,8 *")); } }