Jenkins Pipeline NotSerializableException: groovy.json.internal.LazyMap

80

Gelöst : Dank der unten stehenden Antwort von S.Richmond. Ich musste alle gespeicherten Karten des groovy.json.internal.LazyMapTyps deaktivieren, was bedeutete, dass die Variablen ungültig wurden envServersund objectnach der Verwendung.

Zusätzlich : Personen, die nach diesem Fehler suchen, könnten daran interessiert sein, readJSONstattdessen den Jenkins-Pipeline-Schritt zu verwenden. Weitere Informationen finden Sie hier .


Ich versuche, Jenkins Pipeline zu verwenden, um Eingaben vom Benutzer zu übernehmen, die als JSON-Zeichenfolge an den Job übergeben werden. Pipeline analysiert dies dann mit dem Slurper und ich suche die wichtigen Informationen aus. Diese Informationen werden dann verwendet, um einen Job mehrmals parallel zu unterschiedlichen Jobparametern auszuführen.

Bis ich den Code unten hinzufüge, "## Error when below here is added"läuft das Skript einwandfrei. Sogar der Code unter diesem Punkt wird von selbst ausgeführt. Aber wenn kombiniert, erhalte ich den folgenden Fehler.

Ich sollte beachten, dass der ausgelöste Job aufgerufen wird und erfolgreich ausgeführt wird, aber der folgende Fehler auftritt und den Hauptjob fehlschlägt. Aus diesem Grund wartet der Hauptjob nicht auf die Rückgabe des ausgelösten Jobs. Ich könnte versuchen, das zu umgehen, build job:aber ich möchte, dass der Hauptjob auf das Ende des ausgelösten Jobs wartet.

Kann hier jemand helfen? Wenn Sie weitere Informationen benötigen, lassen Sie es mich wissen.

Prost

def slurpJSON() {
return new groovy.json.JsonSlurper().parseText(BUILD_CHOICES);
}

node {
  stage 'Prepare';
  echo 'Loading choices as build properties';
  def object = slurpJSON();

  def serverChoices = [];
  def serverChoicesStr = '';

  for (env in object) {
     envName = env.name;
     envServers = env.servers;

     for (server in envServers) {
        if (server.Select) {
            serverChoicesStr += server.Server;
            serverChoicesStr += ',';
        }
     }
  }
  serverChoicesStr = serverChoicesStr[0..-2];

  println("Server choices: " + serverChoicesStr);

  ## Error when below here is added

  stage 'Jobs'
  build job: 'Dummy Start App', parameters: [[$class: 'StringParameterValue', name: 'SERVER_NAME', value: 'TestServer'], [$class: 'StringParameterValue', name: 'SERVER_DOMAIN', value: 'domain.uk'], [$class: 'StringParameterValue', name: 'APP', value: 'application1']]

}

Error:

java.io.NotSerializableException: groovy.json.internal.LazyMap
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:860)
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:569)
    at org.jboss.marshalling.river.BlockMarshaller.doWriteObject(BlockMarshaller.java:65)
    at org.jboss.marshalling.river.BlockMarshaller.writeObject(BlockMarshaller.java:56)
    at org.jboss.marshalling.MarshallerObjectOutputStream.writeObjectOverride(MarshallerObjectOutputStream.java:50)
    at org.jboss.marshalling.river.RiverObjectOutputStream.writeObjectOverride(RiverObjectOutputStream.java:179)
    at java.io.ObjectOutputStream.writeObject(Unknown Source)
    at java.util.LinkedHashMap.internalWriteEntries(Unknown Source)
    at java.util.HashMap.writeObject(Unknown Source)
...
...
Caused by: an exception which occurred:
    in field delegate
    in field closures
    in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup@5288c
Sunvic
quelle
Bin gerade selbst darauf gestoßen. Haben Sie noch weitere Fortschritte gemacht?
S.Richmond

Antworten:

69

Ich bin heute selbst darauf gestoßen und habe durch Bruteforce herausgefunden, wie ich es lösen kann und warum.

Am besten mit dem Warum beginnen:

Jenkins hat ein Paradigma, bei dem alle Jobs durch Serverneustarts unterbrochen, angehalten und wieder aufgenommen werden können. Um dies zu erreichen, müssen die Pipeline und ihre Daten vollständig serialisierbar sein - dh sie müssen in der Lage sein, den Status von allem zu speichern. Ebenso muss es in der Lage sein, den Status globaler Variablen zwischen Knoten und Unterjobs im Build zu serialisieren. Dies geschieht meiner Meinung nach für Sie und mich und warum es nur auftritt, wenn Sie diesen zusätzlichen Build-Schritt hinzufügen.

Aus irgendeinem Grund sind JSONObjects standardmäßig nicht serialisierbar. Ich bin kein Java-Entwickler, daher kann ich leider nicht viel mehr zu diesem Thema sagen. Es gibt viele Antworten darauf, wie man dies richtig beheben kann, obwohl ich nicht weiß, wie sie auf Groovy und Jenkins anwendbar sind. In diesem Beitrag finden Sie weitere Informationen.

So beheben Sie das Problem:

Wenn Sie wissen wie, können Sie das JSONObject möglicherweise irgendwie serialisierbar machen. Andernfalls können Sie das Problem beheben, indem Sie sicherstellen, dass keine globalen Variablen dieses Typs vorhanden sind.

Versuchen Sie, Ihre Variable zu deaktivieren objectoder in eine Methode zu verpacken, damit ihr Gültigkeitsbereich nicht global ist.

S.Richmond
quelle
2
Danke, das ist der Hinweis, den ich brauchte, um das zu lösen. Während ich Ihren Vorschlag bereits ausprobiert hatte, sah ich ihn noch einmal an und hatte nicht daran gedacht, dass ich Teile der Karte in anderen Variablen speicherte - diese verursachten die Fehler. Also musste ich sie auch deaktivieren. Wird meine Frage ändern, um die korrekten Änderungen am Code aufzunehmen. Prost
Sunvic
1
Dies wird ~ 8 mal am Tag angezeigt. Würde es Ihnen etwas ausmachen, ein detaillierteres Beispiel für die Implementierung dieser Lösung zu liefern?
Jordan Stefanelli
1
Es gibt keine einfache Lösung, da dies davon abhängt, was Sie getan haben. Die hier bereitgestellten Informationen sowie die oben in seinem Beitrag hinzugefügte Lösung @Sunvic sollten ausreichen, um eine Lösung für den eigenen Code zu finden.
S.Richmond
1
Die folgende Lösung mit JsonSlurperClassic, die genau das gleiche Problem behebt, das ich hatte, sollte hier wahrscheinlich die genehmigte Wahl sein. Diese Antwort hat ihre Vorzüge, ist aber nicht die richtige Lösung für dieses spezielle Problem.
Quarz
@ JordanStefanelli Ich habe den Code für meine Lösung gepostet. Siehe meine Antwort unten
Nils El-Himoud
127

Verwenden Sie JsonSlurperClassicstattdessen.

Da Groovy 2.3 ( Anmerkung: Jenkins 2.7.1 verwendet Groovy 2.4.7 ) JsonSlurperzurück LazyMapstatt HashMap. Dies macht die neue Implementierung von JsonSlurper nicht threadsicher und nicht serialisierbar. Dies macht es außerhalb von @ NonDSL-Funktionen in Pipeline-DSL-Skripten unbrauchbar.

Sie können jedoch auf groovy.json.JsonSlurperClassicdas zurückgreifen, das altes Verhalten unterstützt und sicher in Pipeline-Skripten verwendet werden kann.

Beispiel

import groovy.json.JsonSlurperClassic 


@NonCPS
def jsonParse(def json) {
    new groovy.json.JsonSlurperClassic().parseText(json)
}

node('master') {
    def config =  jsonParse(readFile("config.json"))

    def db = config["database"]["address"]
    ...
}    

ps. Sie müssen noch genehmigen, JsonSlurperClassicbevor es aufgerufen werden kann.

luka5z
quelle
2
Könnten Sie mir bitte sagen, wie ich genehmigen soll JsonSlurperClassic?
Mybecks
7
Der Jenkins-Administrator muss zu Jenkins verwalten »In-Process-Skriptgenehmigung navigieren.
luka5z
Leider bekomme ich nurhudson.remoting.ProxyException: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed: Script1.groovy: 24: unable to resolve class groovy.json.JsonSlurperClassic
dvtoever
13
JsonSluperClassic .. Dieser Name sagt viel über den aktuellen Stand der Softwareentwicklung aus
Marcos Brigante
1
Vielen Dank für diese ausführliche Erklärung. Du hast mir viel Zeit gespart. Diese Lösung funktioniert wie ein Zauber in meiner Jenkins-Pipeline.
Sathish Prakasam
16

BEARBEITEN: Wie von @Sunvic in den Kommentaren hervorgehoben, funktioniert die folgende Lösung für JSON-Arrays nicht wie sie ist.

Ich habe mich damit befasst, indem ich aus den faulen Ergebnissen JsonSlurperein neues erstellt habe HashMap. HashMapist Serializable.

Ich glaube, dass dies eine Whitelist sowohl der new HashMap(Map)als auch der erforderlich machte JsonSlurper.

@NonCPS
def parseJsonText(String jsonText) {
  final slurper = new JsonSlurper()
  return new HashMap<>(slurper.parseText(jsonText))
}

Insgesamt würde ich empfehlen, nur das Plugin " Pipeline Utility Steps" zu verwenden , da es einen readJSONSchritt enthält , der entweder Dateien im Arbeitsbereich oder Text unterstützt.

mkobit
quelle
1
Hat bei mir nicht funktioniert - immer wieder ein Fehler Could not find matching constructor for: java.util.HashMap(java.util.ArrayList). Die Dokumentation schlägt vor, eine Liste oder eine Karte auszuspucken. Wie konfigurieren Sie die Rückgabe einer Karte?
Sunvic
@ Sunvic Guter Fang, die Daten, die wir analysiert haben, sind immer Objekte, niemals JSON-Arrays. Versuchen Sie, ein JSON-Array zu analysieren?
mkobit
Ah ja, es ist ein JSON-Array, das wird es sein.
Sunvic
Sowohl diese Antwort und unten, auf Jenkins, hob eine RejectedEception weil Jenkins groovy läuft im Sandbox - env
yiwen
@yiwen Ich erwähnte, dass es eine Administrator-Whitelist erfordert, aber vielleicht könnte die Antwort geklärt werden, was das bedeutet?
mkobit
7

Ich möchte eine der Antworten positiv bewerten: Ich würde empfehlen, nur das Pipeline Utility Steps-Plugin zu verwenden, da es einen readJSON-Schritt enthält, der entweder Dateien im Arbeitsbereich oder Text unterstützt: https://jenkins.io/doc/pipeline/steps / Pipeline-Utility-Schritte / # readjson-read-json-from-files-in-the-workspace

script{
  def foo_json = sh(returnStdout:true, script: "aws --output json XXX").trim()
  def foo = readJSON text: foo_json
}

Dies erfordert KEINE Whitelist oder zusätzliche Inhalte.

Regnoult
quelle
5

Eine etwas allgemeinere Form der Antwort von @mkobit, die das Dekodieren von Arrays und Karten ermöglichen würde, wäre:

import groovy.json.JsonSlurper

@NonCPS
def parseJsonText(String json) {
  def object = new JsonSlurper().parseText(json)
  if(object instanceof groovy.json.internal.LazyMap) {
      return new HashMap<>(object)
  }
  return object
}

HINWEIS: Beachten Sie, dass dadurch nur das LazyMap-Objekt der obersten Ebene in eine HashMap konvertiert wird. Verschachtelte LazyMap-Objekte sind weiterhin vorhanden und verursachen weiterhin Probleme mit Jenkins.

TomDotTom
quelle
5

Dies ist die detaillierte Antwort, nach der gefragt wurde.

Das Unset hat bei mir funktioniert:

String res = sh(script: "curl --header 'X-Vault-Token: ${token}' --request POST --data '${payload}' ${url}", returnStdout: true)
def response = new JsonSlurper().parseText(res)
String value1 = response.data.value1
String value2 = response.data.value2

// unset response because it's not serializable and Jenkins throws NotSerializableException.
response = null

Ich habe die Werte aus der analysierten Antwort gelesen und wenn ich das Objekt nicht mehr benötige, habe ich es deaktiviert.

Nils El-Himoud
quelle
2

Die Art und Weise, wie das Pipeline-Plugin implementiert wurde, hat schwerwiegende Auswirkungen auf nicht trivialen Groovy-Code. Dieser Link erklärt, wie mögliche Probleme vermieden werden können: https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md#serializing-local-variables

In Ihrem speziellen Fall würde ich in Betracht ziehen, anstelle von JSON-Objekten @NonCPSAnmerkungen zu slurpJSONMap-of-Maps hinzuzufügen und diese zurückzugeben. Der Code sieht nicht nur sauberer aus, sondern ist auch effizienter, insbesondere wenn dieser JSON komplex ist.

Marcin Płonka
quelle
2

Gemäß den im Jenkins-Blog veröffentlichten Best Practices ( Best Practice für die Skalierbarkeit von Pipelines ) wird dringend empfohlen, Befehlszeilentools oder Skripte für diese Art von Arbeit zu verwenden:

Gotcha: Vermeiden Sie insbesondere das Parsen von Pipeline-XML oder JSON mit Groovys XmlSlurper und JsonSlurper! Bevorzugen Sie dringend Befehlszeilentools oder Skripte.

ich. Die Groovy-Implementierungen sind komplex und daher bei der Verwendung von Pipelines spröder.

ii. XmlSlurper und JsonSlurper können in Pipelines hohe Speicher- und CPU-Kosten verursachen

iii. xmllint und xmlstartlet sind Befehlszeilentools, die XML-Extraktion über xpath ermöglichen

iv. jq bietet die gleiche Funktionalität für JSON

v. Diese Extraktionswerkzeuge können mit curl oder wget gekoppelt sein, um Informationen von einer HTTP-API abzurufen

Daher wird erklärt, warum die meisten auf dieser Seite vorgeschlagenen Lösungen standardmäßig von der Sandbox des Jenkins-Sicherheitsskript-Plugins blockiert werden.

Die Sprachphilosophie von Groovy ist Bash näher als Python oder Java. Es bedeutet auch, dass es nicht selbstverständlich ist, komplexe und schwere Arbeiten in Native Groovy auszuführen.

Vor diesem Hintergrund habe ich mich persönlich für Folgendes entschieden:

sh('jq <filters_and_options> file.json')

Weitere Hilfe finden Sie im jq-Handbuch und unter Objekte mit jq-Stapelüberlauf auswählen .

Dies ist etwas kontraintuitiv, da Groovy viele generische Methoden bereitstellt, die nicht in der Standard-Whitelist enthalten sind.

Wenn Sie sich für den Großteil Ihrer Arbeit ohnehin für die Verwendung der Sprache Groovy mit aktivierter und sauberer Sandbox entscheiden (was nicht einfach, weil nicht natürlich ist), empfehle ich Ihnen, die Whitelists für die Version Ihres Sicherheitsskript-Plugins zu überprüfen, um zu erfahren, welche Möglichkeiten Sie haben: Skript Whitelists für Sicherheits-Plugins

Vhamon
quelle
2

Mit der folgenden Funktion können Sie LazyMap in eine reguläre LinkedHashMap konvertieren (die Reihenfolge der Originaldaten bleibt erhalten):

LinkedHashMap nonLazyMap (Map lazyMap) {
    LinkedHashMap res = new LinkedHashMap()
    lazyMap.each { key, value ->
        if (value instanceof Map) {
            res.put (key, nonLazyMap(value))
        } else if (value instanceof List) {
            res.put (key, value.stream().map { it instanceof Map ? nonLazyMap(it) : it }.collect(Collectors.toList()))
        } else {
            res.put (key, value)
        }
    }
    return res
}

... 

LazyMap lazyMap = new JsonSlurper().parseText (jsonText)
Map serializableMap = nonLazyMap(lazyMap);

oder verwenden Sie besser einen readJSON-Schritt, wie in früheren Kommentaren erwähnt:

Map serializableMap = readJSON text: jsonText
Sergey P.
quelle
1

Die anderen Ideen in diesem Beitrag waren hilfreich, aber nicht alles, wonach ich gesucht habe - also habe ich die Teile extrahiert, die meinen Anforderungen entsprechen, und einige meiner eigenen Magix hinzugefügt ...

def jsonSlurpLaxWithoutSerializationTroubles(String jsonText)
{
    return new JsonSlurperClassic().parseText(
        new JsonBuilder(
            new JsonSlurper()
                .setType(JsonParserType.LAX)
                .parseText(jsonText)
        )
        .toString()
    )
}

Ja, wie ich in meinem eigenen Git-Commit des Codes festgestellt habe: "Wild ineffizienter, aber winziger Koeffizient: JSON-Slurp-Lösung" (mit dem ich für diesen Zweck einverstanden bin). Die Aspekte, die ich lösen musste:

  1. Vermeiden Sie das java.io.NotSerializableExceptionProblem vollständig , auch wenn der JSON-Text verschachtelte Container definiert
  2. Arbeiten Sie sowohl für Karten- als auch für Array-Container
  3. Unterstützen Sie das LAX-Parsing (der wichtigste Teil für meine Situation)
  4. Einfach zu implementieren (selbst mit den umständlichen verschachtelten Konstruktoren, die das vermeiden @NonCPS)
Stevel
quelle
1

Noob Fehler meinerseits. Jemandes Code aus einem alten Pipeline-Plugin verschoben, Jenkins 1.6? auf einen Server mit den neuesten 2.x Jenkins.

Aus diesem Grund fehlgeschlagen: "java.io.NotSerializableException: groovy.lang.IntRange" Ich habe diesen Beitrag für den obigen Fehler mehrmals gelesen und gelesen. Realisiert: für (num in 1..numSlaves) {IntRange - nicht serialisierbarer Objekttyp.

In einfacher Form umgeschrieben: für (num = 1; num <= numSlaves; num ++)

Alles ist gut mit der Welt.

Ich benutze Java oder Groovy nicht sehr oft.

Danke Leute.

mpechner
quelle
0

Ich habe einen einfacheren Weg in Off-Docs für die Jenkins-Pipeline gefunden

Arbeitsbeispiel

import groovy.json.JsonSlurperClassic 


@NonCPS
def jsonParse(def json) {
    new groovy.json.JsonSlurperClassic().parseText(json)
}

@NonCPS
def jobs(list) {
    list
        .grep { it.value == true  }
        .collect { [ name : it.key.toString(),
                      branch : it.value.toString() ] }

}

node {
    def params = jsonParse(env.choice_app)
    def forBuild = jobs(params)
}

Aufgrund von Einschränkungen im Workflow, dh JENKINS-26481, ist es nicht wirklich möglich, Groovy-Closures oder eine von Closures abhängige Syntax zu verwenden. Daher können Sie den Groovy-Standard für die Verwendung von .collectEntries in einer Liste und das Generieren der Schritte als Werte nicht ausführen für die resultierenden Einträge. Sie können auch nicht die Standard-Java-Syntax für For-Schleifen verwenden, dh "for (String s: Strings)", sondern müssen für Zähler Schleifen der alten Schule verwenden.

Kirill K.
quelle
1
Ich würde empfehlen, stattdessen den Jenkins-Pipeline-Schritt readJSON zu verwenden. Weitere Informationen finden Sie hier .
Sunvic